refactor(MySQL) (#22619)

### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Ciro Spaciari
2025-09-13 14:52:19 -07:00
committed by GitHub
parent 8e786c1cfc
commit beea7180f3
30 changed files with 2238 additions and 1901 deletions

View File

@@ -5,21 +5,21 @@ pub fn createBinding(globalObject: *jsc.JSGlobalObject) JSValue {
binding.put(
globalObject,
ZigString.static("createQuery"),
jsc.JSFunction.create(globalObject, "createQuery", MySQLQuery.call, 6, .{}),
jsc.JSFunction.create(globalObject, "createQuery", MySQLQuery.createInstance, 6, .{}),
);
binding.put(
globalObject,
ZigString.static("createConnection"),
jsc.JSFunction.create(globalObject, "createQuery", MySQLConnection.call, 2, .{}),
jsc.JSFunction.create(globalObject, "createConnection", MySQLConnection.createInstance, 2, .{}),
);
return binding;
}
pub const MySQLConnection = @import("./mysql/MySQLConnection.zig");
pub const MySQLConnection = @import("./mysql/js/JSMySQLConnection.zig");
pub const MySQLContext = @import("./mysql/MySQLContext.zig");
pub const MySQLQuery = @import("./mysql/MySQLQuery.zig");
pub const MySQLQuery = @import("./mysql/js/JSMySQLQuery.zig");
const bun = @import("bun");

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +1,18 @@
const MySQLQuery = @This();
const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", deinit, .{});
statement: ?*MySQLStatement = null,
query: bun.String = bun.String.empty,
cursor_name: bun.String = bun.String.empty,
thisValue: JSRef = JSRef.empty(),
#statement: ?*MySQLStatement = null,
#query: bun.String,
status: Status = Status.pending,
ref_count: RefCount = RefCount.init(),
flags: packed struct(u8) {
is_done: bool = false,
binary: bool = false,
#status: Status,
#flags: packed struct(u8) {
bigint: bool = false,
simple: bool = false,
pipelined: bool = false,
result_mode: SQLQueryResultMode = .objects,
_padding: u1 = 0,
} = .{},
pub const ref = RefCount.ref;
pub const deref = RefCount.deref;
pub const Status = enum(u8) {
/// The query was just enqueued, statement status can be checked for more details
pending,
/// The query is being bound to the statement
binding,
/// The query is running
running,
/// The query is waiting for a partial response
partial_response,
/// The query was successful
success,
/// The query failed
fail,
pub fn isRunning(this: Status) bool {
return @intFromEnum(this) > @intFromEnum(Status.pending) and @intFromEnum(this) < @intFromEnum(Status.success);
}
};
pub fn hasPendingActivity(this: *@This()) bool {
return this.ref_count.load(.monotonic) > 1;
}
pub fn deinit(this: *@This()) void {
this.thisValue.deinit();
if (this.statement) |statement| {
statement.deref();
}
this.query.deref();
this.cursor_name.deref();
bun.default_allocator.destroy(this);
}
pub fn finalize(this: *@This()) void {
debug("MySQLQuery finalize", .{});
// Clean up any statement reference
if (this.statement) |statement| {
statement.deref();
this.statement = null;
}
if (this.thisValue == .weak) {
// clean up if is a weak reference, if is a strong reference we need to wait until the query is done
// if we are a strong reference, here is probably a bug because GC'd should not happen
this.thisValue.weak = .zero;
}
this.deref();
}
pub fn onWriteFail(
this: *@This(),
err: AnyMySQLError.Error,
globalObject: *jsc.JSGlobalObject,
queries_array: JSValue,
) void {
this.status = .fail;
const thisValue = this.thisValue.get();
defer this.thisValue.deinit();
const targetValue = this.getTarget(globalObject, true);
if (thisValue == .zero or targetValue == .zero) {
return;
}
const instance = AnyMySQLError.mysqlErrorToJS(globalObject, "Failed to bind query", err);
const vm = jsc.VirtualMachine.get();
const function = vm.rareData().mysql_context.onQueryRejectFn.get().?;
const event_loop = vm.eventLoop();
event_loop.runCallback(function, globalObject, thisValue, &.{
targetValue,
// TODO: add mysql error to JS
// postgresErrorToJS(globalObject, null, err),
instance,
queries_array,
});
}
pub fn bindAndExecute(this: *MySQLQuery, writer: anytype, statement: *MySQLStatement, globalObject: *jsc.JSGlobalObject) AnyMySQLError.Error!void {
debug("bindAndExecute", .{});
bun.assertf(statement.params.len == statement.params_received and statement.statement_id > 0, "statement is not prepared", .{});
if (statement.signature.fields.len != statement.params.len) {
return error.WrongNumberOfParametersProvided;
}
var packet = try writer.start(0);
var execute = PreparedStatement.Execute{
.statement_id = statement.statement_id,
.param_types = statement.signature.fields,
.new_params_bind_flag = statement.execution_flags.need_to_send_params,
.iteration_count = 1,
};
statement.execution_flags.need_to_send_params = false;
defer execute.deinit();
try this.bind(&execute, globalObject);
try execute.write(writer);
try packet.end();
this.status = .running;
}
fn bind(this: *MySQLQuery, execute: *PreparedStatement.Execute, globalObject: *jsc.JSGlobalObject) AnyMySQLError.Error!void {
const thisValue = this.thisValue.get();
const binding_value = js.bindingGetCached(thisValue) orelse .zero;
const columns_value = js.columnsGetCached(thisValue) orelse .zero;
_padding: u3 = 0,
},
fn bind(this: *MySQLQuery, execute: *PreparedStatement.Execute, globalObject: *JSGlobalObject, binding_value: JSValue, columns_value: JSValue) AnyMySQLError.Error!void {
var iter = try QueryBindingIterator.init(binding_value, columns_value, globalObject);
var i: u32 = 0;
@@ -140,7 +25,6 @@ fn bind(this: *MySQLQuery, execute: *PreparedStatement.Execute, globalObject: *j
}
while (try iter.next()) |js_value| {
const param = execute.param_types[i];
debug("param: {s} unsigned? {}", .{ @tagName(param.type), param.flags.UNSIGNED });
params[i] = try Value.fromJS(
js_value,
globalObject,
@@ -154,401 +38,245 @@ fn bind(this: *MySQLQuery, execute: *PreparedStatement.Execute, globalObject: *j
return error.InvalidQueryBinding;
}
this.status = .binding;
this.#status = .binding;
execute.params = params;
}
pub fn onError(this: *@This(), err: ErrorPacket, globalObject: *jsc.JSGlobalObject) void {
debug("onError", .{});
this.onJSError(err.toJS(globalObject), globalObject);
fn bindAndExecute(this: *MySQLQuery, writer: anytype, statement: *MySQLStatement, globalObject: *JSGlobalObject, binding_value: JSValue, columns_value: JSValue) AnyMySQLError.Error!void {
bun.assertf(statement.params.len == statement.params_received and statement.statement_id > 0, "statement is not prepared", .{});
if (statement.signature.fields.len != statement.params.len) {
return error.WrongNumberOfParametersProvided;
}
var packet = try writer.start(0);
var execute = PreparedStatement.Execute{
.statement_id = statement.statement_id,
.param_types = statement.signature.fields,
.new_params_bind_flag = statement.execution_flags.need_to_send_params,
.iteration_count = 1,
};
statement.execution_flags.need_to_send_params = false;
defer execute.deinit();
try this.bind(&execute, globalObject, binding_value, columns_value);
try execute.write(writer);
try packet.end();
this.#status = .running;
}
pub fn onJSError(this: *@This(), err: jsc.JSValue, globalObject: *jsc.JSGlobalObject) void {
this.ref();
defer this.deref();
this.status = .fail;
const thisValue = this.thisValue.get();
defer this.thisValue.deinit();
const targetValue = this.getTarget(globalObject, true);
if (thisValue == .zero or targetValue == .zero) {
fn runSimpleQuery(this: *@This(), connection: *MySQLConnection) !void {
if (this.#status != .pending or !connection.canExecuteQuery()) {
debug("cannot execute query", .{});
// cannot execute query
return;
}
var vm = jsc.VirtualMachine.get();
const function = vm.rareData().mysql_context.onQueryRejectFn.get().?;
const event_loop = vm.eventLoop();
event_loop.runCallback(function, globalObject, thisValue, &.{
targetValue,
err,
});
}
pub fn getTarget(this: *@This(), globalObject: *jsc.JSGlobalObject, clean_target: bool) jsc.JSValue {
const thisValue = this.thisValue.tryGet() orelse return .zero;
const target = js.targetGetCached(thisValue) orelse return .zero;
if (clean_target) {
js.targetSetCached(thisValue, globalObject, .zero);
var query_str = this.#query.toUTF8(bun.default_allocator);
defer query_str.deinit();
const writer = connection.getWriter();
if (this.#statement == null) {
const stmt = bun.new(MySQLStatement, .{
.signature = Signature.empty(),
.status = .parsing,
.ref_count = .initExactRefs(1),
});
this.#statement = stmt;
}
return target;
try MySQLRequest.executeQuery(query_str.slice(), MySQLConnection.Writer, writer);
this.#status = .running;
}
fn consumePendingValue(thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject) ?JSValue {
const pending_value = js.pendingValueGetCached(thisValue) orelse return null;
js.pendingValueSetCached(thisValue, globalObject, .zero);
return pending_value;
}
fn runPreparedQuery(
this: *@This(),
connection: *MySQLConnection,
globalObject: *JSGlobalObject,
columns_value: JSValue,
binding_value: JSValue,
) !void {
var query_str: ?bun.ZigString.Slice = null;
defer if (query_str) |str| str.deinit();
pub fn allowGC(thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject) void {
if (thisValue == .zero) {
return;
}
if (this.#statement == null) {
const query = this.#query.toUTF8(bun.default_allocator);
query_str = query;
var signature = Signature.generate(globalObject, query.slice(), binding_value, columns_value) catch |err| {
if (!globalObject.hasException())
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to generate signature", err));
return error.JSError;
};
errdefer signature.deinit();
const entry = connection.getStatementFromSignatureHash(bun.hash(signature.name)) catch |err| {
return globalObject.throwError(err, "failed to allocate statement");
};
defer thisValue.ensureStillAlive();
js.bindingSetCached(thisValue, globalObject, .zero);
js.pendingValueSetCached(thisValue, globalObject, .zero);
js.targetSetCached(thisValue, globalObject, .zero);
}
fn u64ToJSValue(value: u64) JSValue {
if (value <= jsc.MAX_SAFE_INTEGER) {
return JSValue.jsNumber(value);
}
return JSValue.jsBigInt(value);
}
pub fn onResult(this: *@This(), result_count: u64, globalObject: *jsc.JSGlobalObject, connection: jsc.JSValue, is_last: bool, last_insert_id: u64, affected_rows: u64) void {
this.ref();
defer this.deref();
const thisValue = this.thisValue.get();
const targetValue = this.getTarget(globalObject, is_last);
if (is_last) {
this.status = .success;
} else {
this.status = .partial_response;
}
defer if (is_last) {
allowGC(thisValue, globalObject);
this.thisValue.deinit();
};
if (thisValue == .zero or targetValue == .zero) {
return;
}
const vm = jsc.VirtualMachine.get();
const function = vm.rareData().mysql_context.onQueryResolveFn.get().?;
const event_loop = vm.eventLoop();
const tag: CommandTag = .{ .SELECT = result_count };
event_loop.runCallback(function, globalObject, thisValue, &.{
targetValue,
consumePendingValue(thisValue, globalObject) orelse .js_undefined,
tag.toJSTag(globalObject),
tag.toJSNumber(),
if (connection == .zero) .js_undefined else MySQLConnection.js.queriesGetCached(connection) orelse .js_undefined,
JSValue.jsBoolean(is_last),
JSValue.jsNumber(last_insert_id),
JSValue.jsNumber(affected_rows),
});
}
pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*MySQLQuery {
_ = callframe;
return globalThis.throw("MySQLQuery cannot be constructed directly", .{});
}
pub fn estimatedSize(this: *MySQLQuery) usize {
_ = this;
return @sizeOf(MySQLQuery);
}
pub fn call(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments();
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
const query = args.nextEat() orelse {
return globalThis.throw("query must be a string", .{});
};
const values = args.nextEat() orelse {
return globalThis.throw("values must be an array", .{});
};
if (!query.isString()) {
return globalThis.throw("query must be a string", .{});
}
if (values.jsType() != .Array) {
return globalThis.throw("values must be an array", .{});
}
const pending_value: JSValue = args.nextEat() orelse .js_undefined;
const columns: JSValue = args.nextEat() orelse .js_undefined;
const js_bigint: JSValue = args.nextEat() orelse .false;
const js_simple: JSValue = args.nextEat() orelse .false;
const bigint = js_bigint.isBoolean() and js_bigint.asBoolean();
const simple = js_simple.isBoolean() and js_simple.asBoolean();
if (simple) {
if (try values.getLength(globalThis) > 0) {
return globalThis.throwInvalidArguments("simple query cannot have parameters", .{});
}
if (try query.getLength(globalThis) >= std.math.maxInt(i32)) {
return globalThis.throwInvalidArguments("query is too long", .{});
if (entry.found_existing) {
const stmt = entry.value_ptr.*;
if (stmt.status == .failed) {
const error_response = stmt.error_response.toJS(globalObject);
// If the statement failed, we need to throw the error
return globalObject.throwValue(error_response);
}
this.#statement = stmt;
stmt.ref();
signature.deinit();
signature = Signature{};
} else {
const stmt = bun.new(MySQLStatement, .{
.signature = signature,
.ref_count = .initExactRefs(2),
.status = .pending,
.statement_id = 0,
});
this.#statement = stmt;
entry.value_ptr.* = stmt;
}
}
if (!pending_value.jsType().isArrayLike()) {
return globalThis.throwInvalidArgumentType("query", "pendingValue", "Array");
const stmt = this.#statement.?;
switch (stmt.status) {
.failed => {
debug("failed", .{});
const error_response = stmt.error_response.toJS(globalObject);
// If the statement failed, we need to throw the error
return globalObject.throwValue(error_response);
},
.prepared => {
if (connection.canPipeline()) {
debug("bindAndExecute", .{});
const writer = connection.getWriter();
this.bindAndExecute(writer, stmt, globalObject, binding_value, columns_value) catch |err| {
if (!globalObject.hasException())
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to bind and execute query", err));
return error.JSError;
};
this.#flags.pipelined = true;
}
},
.parsing => {
debug("parsing", .{});
},
.pending => {
if (connection.canPrepareQuery()) {
debug("prepareRequest", .{});
const writer = connection.getWriter();
const query = query_str orelse this.#query.toUTF8(bun.default_allocator);
MySQLRequest.prepareRequest(query.slice(), MySQLConnection.Writer, writer) catch |err| {
return globalObject.throwError(err, "failed to prepare query");
};
stmt.status = .parsing;
}
},
}
}
var ptr = bun.default_allocator.create(MySQLQuery) catch |err| {
return globalThis.throwError(err, "failed to allocate query");
};
const this_value = ptr.toJS(globalThis);
this_value.ensureStillAlive();
ptr.* = .{
.query = try query.toBunString(globalThis),
.thisValue = JSRef.initWeak(this_value),
.flags = .{
pub fn init(query: bun.String, bigint: bool, simple: bool) @This() {
query.ref();
return .{
.#query = query,
.#status = .pending,
.#flags = .{
.bigint = bigint,
.simple = simple,
},
};
ptr.query.ref();
}
js.bindingSetCached(this_value, globalThis, values);
js.pendingValueSetCached(this_value, globalThis, pending_value);
if (!columns.isUndefined()) {
js.columnsSetCached(this_value, globalThis, columns);
pub fn runQuery(this: *@This(), connection: *MySQLConnection, globalObject: *JSGlobalObject, columns_value: JSValue, binding_value: JSValue) !void {
if (this.#flags.simple) {
debug("runSimpleQuery", .{});
return try this.runSimpleQuery(connection);
}
debug("runPreparedQuery", .{});
return try this.runPreparedQuery(
connection,
globalObject,
if (columns_value == .zero) .js_undefined else columns_value,
if (binding_value == .zero) .js_undefined else binding_value,
);
}
return this_value;
pub inline fn setResultMode(this: *@This(), result_mode: SQLQueryResultMode) void {
this.#flags.result_mode = result_mode;
}
pub fn setPendingValue(this: *@This(), globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const result = callframe.argument(0);
const thisValue = this.thisValue.tryGet() orelse return .js_undefined;
js.pendingValueSetCached(thisValue, globalObject, result);
return .js_undefined;
pub inline fn result(this: *@This(), is_last_result: bool) bool {
if (this.#status == .success or this.#status == .fail) return false;
this.#status = if (is_last_result) .success else .partial_response;
return true;
}
pub fn setMode(this: *@This(), globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const js_mode = callframe.argument(0);
if (js_mode.isEmptyOrUndefinedOrNull() or !js_mode.isNumber()) {
return globalObject.throwInvalidArgumentType("setMode", "mode", "Number");
pub fn fail(this: *@This()) bool {
if (this.#status == .fail or this.#status == .success) return false;
this.#status = .fail;
return true;
}
pub fn cleanup(this: *@This()) void {
if (this.#statement) |statement| {
statement.deref();
this.#statement = null;
}
const mode = try js_mode.coerce(i32, globalObject);
this.flags.result_mode = std.meta.intToEnum(SQLQueryResultMode, mode) catch {
return globalObject.throwInvalidArgumentTypeValue("mode", "Number", js_mode);
};
return .js_undefined;
var query = this.#query;
defer query.deref();
this.#query = bun.String.empty;
}
pub fn doDone(this: *@This(), globalObject: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
_ = globalObject;
this.flags.is_done = true;
return .js_undefined;
pub inline fn isCompleted(this: *const @This()) bool {
return this.#status == .success or this.#status == .fail;
}
pub fn doCancel(this: *MySQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
_ = callframe;
_ = globalObject;
_ = this;
return .js_undefined;
}
pub fn doRun(this: *MySQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
debug("doRun", .{});
var arguments = callframe.arguments();
const connection: *MySQLConnection = arguments[0].as(MySQLConnection) orelse {
return globalObject.throw("connection must be a MySQLConnection", .{});
};
connection.poll_ref.ref(globalObject.bunVM());
var query = arguments[1];
if (!query.isObject()) {
return globalObject.throwInvalidArgumentType("run", "query", "Query");
pub inline fn isRunning(this: *const @This()) bool {
switch (this.#status) {
.running, .binding, .partial_response => return true,
.success, .fail, .pending => return false,
}
}
pub inline fn isPending(this: *const @This()) bool {
return this.#status == .pending;
}
const this_value = callframe.this();
const binding_value = js.bindingGetCached(this_value) orelse .zero;
var query_str = this.query.toUTF8(bun.default_allocator);
defer query_str.deinit();
const writer = connection.writer();
// We need a strong reference to the query so that it doesn't get GC'd
this.ref();
const can_execute = connection.canExecuteQuery();
if (this.flags.simple) {
// simple queries are always text in MySQL
this.flags.binary = false;
debug("executeQuery", .{});
pub inline fn isBeingPrepared(this: *@This()) bool {
return this.#status == .pending and this.#statement != null and this.#statement.?.status == .parsing;
}
const stmt = bun.default_allocator.create(MySQLStatement) catch {
this.deref();
return globalObject.throwOutOfMemory();
};
// Query is simple and it's the only owner of the statement
stmt.* = .{
.signature = Signature.empty(),
.status = .parsing,
};
this.statement = stmt;
if (can_execute) {
connection.sequence_id = 0;
MySQLRequest.executeQuery(query_str.slice(), MySQLConnection.Writer, writer) catch |err| {
debug("executeQuery failed: {s}", .{@errorName(err)});
// fail to run do cleanup
this.statement = null;
bun.default_allocator.destroy(stmt);
this.deref();
if (!globalObject.hasException())
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to execute query", err));
return error.JSError;
};
connection.flags.is_ready_for_query = false;
connection.nonpipelinable_requests += 1;
this.status = .running;
} else {
this.status = .pending;
}
connection.requests.writeItem(this) catch {
// fail to run do cleanup
this.statement = null;
bun.default_allocator.destroy(stmt);
this.deref();
return globalObject.throwOutOfMemory();
};
debug("doRun: wrote query to queue", .{});
this.thisValue.upgrade(globalObject);
js.targetSetCached(this_value, globalObject, query);
connection.flushDataAndResetTimeout();
return .js_undefined;
}
// prepared statements are always binary in MySQL
this.flags.binary = true;
const columns_value = js.columnsGetCached(callframe.this()) orelse .js_undefined;
var signature = Signature.generate(globalObject, query_str.slice(), binding_value, columns_value) catch |err| {
this.deref();
if (!globalObject.hasException())
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to generate signature", err));
return error.JSError;
};
errdefer signature.deinit();
const entry = connection.statements.getOrPut(bun.default_allocator, bun.hash(signature.name)) catch |err| {
this.deref();
return globalObject.throwError(err, "failed to allocate statement");
};
var did_write = false;
enqueue: {
if (entry.found_existing) {
const stmt = entry.value_ptr.*;
this.statement = stmt;
stmt.ref();
signature.deinit();
signature = Signature{};
switch (stmt.status) {
.failed => {
this.statement = null;
const error_response = stmt.error_response.toJS(globalObject);
stmt.deref();
this.deref();
// If the statement failed, we need to throw the error
return globalObject.throwValue(error_response);
},
.prepared => {
if (can_execute or connection.canPipeline()) {
debug("doRun: binding and executing query", .{});
this.bindAndExecute(writer, this.statement.?, globalObject) catch |err| {
if (!globalObject.hasException())
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to bind and execute query", err));
return error.JSError;
};
connection.sequence_id = 0;
this.flags.pipelined = true;
connection.pipelined_requests += 1;
connection.flags.is_ready_for_query = false;
did_write = true;
}
},
.parsing, .pending => {},
pub inline fn isPipelined(this: *const @This()) bool {
return this.#flags.pipelined;
}
pub inline fn isSimple(this: *const @This()) bool {
return this.#flags.simple;
}
pub inline fn isBigintSupported(this: *const @This()) bool {
return this.#flags.bigint;
}
pub inline fn getResultMode(this: *const @This()) SQLQueryResultMode {
return this.#flags.result_mode;
}
pub inline fn markAsPrepared(this: *@This()) void {
if (this.#status == .pending) {
if (this.#statement) |statement| {
if (statement.status == .parsing and
statement.params.len == statement.params_received and
statement.statement_id > 0)
{
statement.status = .prepared;
}
break :enqueue;
}
const stmt = bun.default_allocator.create(MySQLStatement) catch |err| {
this.deref();
return globalObject.throwError(err, "failed to allocate statement");
};
stmt.* = .{
.signature = signature,
.ref_count = .initExactRefs(2),
.status = .pending,
.statement_id = 0,
};
this.statement = stmt;
entry.value_ptr.* = stmt;
}
this.status = if (did_write) .running else .pending;
try connection.requests.writeItem(this);
this.thisValue.upgrade(globalObject);
js.targetSetCached(this_value, globalObject, query);
if (!did_write and can_execute) {
debug("doRun: preparing query", .{});
if (connection.canPrepareQuery()) {
this.statement.?.status = .parsing;
MySQLRequest.prepareRequest(query_str.slice(), MySQLConnection.Writer, writer) catch |err| {
this.deref();
return globalObject.throwError(err, "failed to prepare query");
};
connection.flags.waiting_to_prepare = true;
connection.flags.is_ready_for_query = false;
}
}
connection.flushDataAndResetTimeout();
return .js_undefined;
}
comptime {
@export(&jsc.toJSHostFn(call), .{ .name = "MySQLQuery__createInstance" });
pub inline fn getStatement(this: *const @This()) ?*MySQLStatement {
return this.#statement;
}
pub const js = jsc.Codegen.JSMySQLQuery;
pub const fromJS = js.fromJS;
pub const fromJSDirect = js.fromJSDirect;
pub const toJS = js.toJS;
const debug = bun.Output.scoped(.MySQLQuery, .visible);
// TODO: move to shared IF POSSIBLE
const AnyMySQLError = @import("./protocol/AnyMySQLError.zig");
const ErrorPacket = @import("./protocol/ErrorPacket.zig");
const MySQLConnection = @import("./MySQLConnection.zig");
const MySQLConnection = @import("./js/JSMySQLConnection.zig");
const MySQLRequest = @import("./MySQLRequest.zig");
const MySQLStatement = @import("./MySQLStatement.zig");
const PreparedStatement = @import("./protocol/PreparedStatement.zig");
const Signature = @import("./protocol/Signature.zig");
const bun = @import("bun");
const std = @import("std");
const CommandTag = @import("../postgres/CommandTag.zig").CommandTag;
const QueryBindingIterator = @import("../shared/QueryBindingIterator.zig").QueryBindingIterator;
const SQLQueryResultMode = @import("../shared/SQLQueryResultMode.zig").SQLQueryResultMode;
const Status = @import("./QueryStatus.zig").Status;
const Value = @import("./MySQLTypes.zig").Value;
const jsc = bun.jsc;
const JSRef = jsc.JSRef;
const JSValue = jsc.JSValue;
const JSGlobalObject = bun.jsc.JSGlobalObject;
const JSValue = bun.jsc.JSValue;

View File

@@ -0,0 +1,4 @@
result_count: u64,
last_insert_id: u64,
affected_rows: u64,
is_last_result: bool,

View File

@@ -0,0 +1,224 @@
pub const MySQLRequestQueue = @This();
#requests: Queue,
#pipelined_requests: u32 = 0,
#nonpipelinable_requests: u32 = 0,
// TODO: refactor to ENUM
#waiting_to_prepare: bool = false,
#is_ready_for_query: bool = true,
pub inline fn canExecuteQuery(this: *@This(), connection: *MySQLConnection) bool {
return connection.isAbleToWrite() and
this.#is_ready_for_query and
this.#nonpipelinable_requests == 0 and
this.#pipelined_requests == 0;
}
pub inline fn canPrepareQuery(this: *@This(), connection: *MySQLConnection) bool {
return connection.isAbleToWrite() and
this.#is_ready_for_query and
!this.#waiting_to_prepare and
this.#pipelined_requests == 0;
}
pub inline fn markAsReadyForQuery(this: *@This()) void {
this.#is_ready_for_query = true;
}
pub inline fn markAsPrepared(this: *@This()) void {
this.#waiting_to_prepare = false;
if (this.current()) |request| {
debug("markAsPrepared markAsPrepared", .{});
request.markAsPrepared();
}
}
pub inline fn canPipeline(this: *@This(), connection: *MySQLConnection) bool {
if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING)) {
@branchHint(.unlikely);
return false;
}
return this.#is_ready_for_query and
this.#nonpipelinable_requests == 0 and // need to wait for non pipelinable requests to finish
!this.#waiting_to_prepare and
connection.isAbleToWrite();
}
pub fn markCurrentRequestAsFinished(this: *@This(), item: *JSMySQLQuery) void {
this.#waiting_to_prepare = false;
if (item.isBeingPrepared()) {
debug("markCurrentRequestAsFinished markAsPrepared", .{});
item.markAsPrepared();
} else if (item.isRunning()) {
if (item.isPipelined()) {
this.#pipelined_requests -= 1;
} else {
this.#nonpipelinable_requests -= 1;
}
}
}
pub fn advance(this: *@This(), connection: *MySQLConnection) void {
var offset: usize = 0;
defer {
while (this.#requests.readableLength() > 0) {
const request = this.#requests.peekItem(0);
// An item may be in the success or failed state and still be inside the queue (see deinit later comments)
// so we do the cleanup her
if (request.isCompleted()) {
debug("isCompleted discard after advance", .{});
this.#requests.discard(1);
request.deref();
continue;
}
break;
}
}
while (this.#requests.readableLength() > offset and connection.isAbleToWrite()) {
var request: *JSMySQLQuery = this.#requests.peekItem(offset);
if (request.isCompleted()) {
if (offset > 0) {
// discard later
offset += 1;
continue;
}
debug("isCompleted", .{});
this.#requests.discard(1);
request.deref();
continue;
}
if (request.isBeingPrepared()) {
debug("isBeingPrepared", .{});
this.#waiting_to_prepare = true;
// cannot continue the queue until the current request is marked as prepared
return;
}
if (request.isRunning()) {
debug("isRunning", .{});
const total_requests_running = this.#pipelined_requests + this.#nonpipelinable_requests;
if (offset < total_requests_running) {
offset += total_requests_running;
} else {
offset += 1;
}
continue;
}
request.run(connection) catch |err| {
debug("run failed", .{});
connection.onError(request, err);
if (offset == 0) {
this.#requests.discard(1);
request.deref();
}
offset += 1;
continue;
};
if (request.isBeingPrepared()) {
debug("isBeingPrepared", .{});
connection.resetConnectionTimeout();
this.#is_ready_for_query = false;
this.#waiting_to_prepare = true;
return;
} else if (request.isRunning()) {
connection.resetConnectionTimeout();
debug("isRunning after run", .{});
this.#is_ready_for_query = false;
if (request.isPipelined()) {
this.#pipelined_requests += 1;
if (this.canPipeline(connection)) {
debug("pipelined requests", .{});
offset += 1;
continue;
}
return;
}
debug("nonpipelinable requests", .{});
this.#nonpipelinable_requests += 1;
}
return;
}
}
pub fn init() @This() {
return .{ .#requests = Queue.init(bun.default_allocator) };
}
pub fn isEmpty(this: *@This()) bool {
return this.#requests.readableLength() == 0;
}
pub fn add(this: *@This(), request: *JSMySQLQuery) void {
debug("add", .{});
if (request.isBeingPrepared()) {
this.#is_ready_for_query = false;
this.#waiting_to_prepare = true;
} else if (request.isRunning()) {
this.#is_ready_for_query = false;
if (request.isPipelined()) {
this.#pipelined_requests += 1;
} else {
this.#nonpipelinable_requests += 1;
}
}
request.ref();
bun.handleOom(this.#requests.writeItem(request));
}
pub fn current(this: *@This()) ?*JSMySQLQuery {
if (this.#requests.readableLength() == 0) {
return null;
}
return this.#requests.peekItem(0);
}
pub fn clean(this: *@This(), reason: ?JSValue, queries_array: JSValue) void {
while (this.current()) |request| {
if (request.isCompleted()) {
request.deref();
this.#requests.discard(1);
continue;
}
if (reason) |r| {
request.rejectWithJSValue(queries_array, r);
} else {
request.reject(queries_array, error.ConnectionClosed);
}
this.#requests.discard(1);
request.deref();
continue;
}
this.#pipelined_requests = 0;
this.#nonpipelinable_requests = 0;
this.#waiting_to_prepare = false;
}
pub fn deinit(this: *@This()) void {
for (this.#requests.readableSlice(0)) |request| {
this.#requests.discard(1);
// We cannot touch JS here
request.markAsFailed();
request.deref();
}
this.#pipelined_requests = 0;
this.#nonpipelinable_requests = 0;
this.#waiting_to_prepare = false;
this.#requests.deinit();
}
const Queue = std.fifo.LinearFifo(*JSMySQLQuery, .Dynamic);
const debug = bun.Output.scoped(.MySQLRequestQueue, .visible);
const JSMySQLQuery = @import("./js/JSMySQLQuery.zig");
const MySQLConnection = @import("./js/JSMySQLConnection.zig");
const bun = @import("bun");
const std = @import("std");
const jsc = bun.jsc;
const JSValue = jsc.JSValue;

View File

@@ -55,7 +55,7 @@ pub fn deinit(this: *MySQLStatement) void {
this.cached_structure.deinit();
this.error_response.deinit();
this.signature.deinit();
bun.default_allocator.destroy(this);
bun.destroy(this);
}
pub fn checkForDuplicateFields(this: *@This()) void {

View File

@@ -0,0 +1,18 @@
pub const Status = enum(u8) {
/// The query was just enqueued, statement status can be checked for more details
pending,
/// The query is being bound to the statement
binding,
/// The query is running
running,
/// The query is waiting for a partial response
partial_response,
/// The query was successful
success,
/// The query failed
fail,
pub fn isRunning(this: Status) bool {
return @intFromEnum(this) > @intFromEnum(Status.pending) and @intFromEnum(this) < @intFromEnum(Status.success);
}
};

View File

@@ -0,0 +1,809 @@
const JSMySQLConnection = @This();
__ref_count: RefCount = RefCount.init(),
#js_value: jsc.JSRef = jsc.JSRef.empty(),
#globalObject: *jsc.JSGlobalObject,
#vm: *jsc.VirtualMachine,
#poll_ref: bun.Async.KeepAlive = .{},
#connection: MySQLConnection,
auto_flusher: AutoFlusher = .{},
idle_timeout_interval_ms: u32 = 0,
connection_timeout_ms: u32 = 0,
/// Before being connected, this is a connection timeout timer.
/// After being connected, this is an idle timeout timer.
timer: bun.api.Timer.EventLoopTimer = .{
.tag = .MySQLConnectionTimeout,
.next = .{
.sec = 0,
.nsec = 0,
},
},
/// This timer controls the maximum lifetime of a connection.
/// It starts when the connection successfully starts (i.e. after handshake is complete).
/// It stops when the connection is closed.
max_lifetime_interval_ms: u32 = 0,
max_lifetime_timer: bun.api.Timer.EventLoopTimer = .{
.tag = .MySQLConnectionMaxLifetime,
.next = .{
.sec = 0,
.nsec = 0,
},
},
pub const ref = RefCount.ref;
pub const deref = RefCount.deref;
pub fn onAutoFlush(this: *@This()) bool {
if (this.#connection.hasBackpressure()) {
this.auto_flusher.registered = false;
// if we have backpressure, wait for onWritable
return false;
}
// drain as much as we can
this.drainInternal();
// if we dont have backpressure and if we still have data to send, return true otherwise return false and wait for onWritable
const keep_flusher_registered = this.#connection.canFlush();
this.auto_flusher.registered = keep_flusher_registered;
return keep_flusher_registered;
}
fn registerAutoFlusher(this: *@This()) void {
if (!this.auto_flusher.registered and // should not be registered
this.#connection.canFlush())
{
AutoFlusher.registerDeferredMicrotaskWithTypeUnchecked(@This(), this, this.#vm);
this.auto_flusher.registered = true;
}
}
fn unregisterAutoFlusher(this: *@This()) void {
if (this.auto_flusher.registered) {
AutoFlusher.unregisterDeferredMicrotaskWithType(@This(), this, this.#vm);
this.auto_flusher.registered = false;
}
}
fn stopTimers(this: *@This()) void {
debug("stopTimers", .{});
if (this.timer.state == .ACTIVE) {
this.#vm.timer.remove(&this.timer);
}
if (this.max_lifetime_timer.state == .ACTIVE) {
this.#vm.timer.remove(&this.max_lifetime_timer);
}
}
fn getTimeoutInterval(this: *@This()) u32 {
return switch (this.#connection.status) {
.connected => {
if (this.#connection.isIdle()) {
return this.idle_timeout_interval_ms;
}
return 0;
},
.failed => 0,
else => {
return this.connection_timeout_ms;
},
};
}
pub fn resetConnectionTimeout(this: *@This()) void {
debug("resetConnectionTimeout", .{});
const interval = this.getTimeoutInterval();
if (this.timer.state == .ACTIVE) {
this.#vm.timer.remove(&this.timer);
}
if (this.#connection.status == .failed or
this.#connection.isProcessingData() or
interval == 0) return;
this.timer.next = bun.timespec.msFromNow(@intCast(interval));
this.#vm.timer.insert(&this.timer);
}
pub fn onConnectionTimeout(this: *@This()) bun.api.Timer.EventLoopTimer.Arm {
this.timer.state = .FIRED;
if (this.#connection.isProcessingData()) {
return .disarm;
}
if (this.#connection.status == .failed) return .disarm;
if (this.getTimeoutInterval() == 0) {
this.resetConnectionTimeout();
return .disarm;
}
switch (this.#connection.status) {
.connected => {
this.failFmt(error.IdleTimeout, "Idle timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.idle_timeout_interval_ms) *| std.time.ns_per_ms)});
},
.connecting => {
this.failFmt(error.ConnectionTimedOut, "Connection timeout after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)});
},
.handshaking,
.authenticating,
.authentication_awaiting_pk,
=> {
this.failFmt(error.ConnectionTimedOut, "Connection timeout after {} (during authentication)", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)});
},
.disconnected, .failed => {},
}
return .disarm;
}
pub fn onMaxLifetimeTimeout(this: *@This()) bun.api.Timer.EventLoopTimer.Arm {
this.max_lifetime_timer.state = .FIRED;
if (this.#connection.status == .failed) return .disarm;
this.failFmt(error.LifetimeTimeout, "Max lifetime timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.max_lifetime_interval_ms) *| std.time.ns_per_ms)});
return .disarm;
}
fn setupMaxLifetimeTimerIfNecessary(this: *@This()) void {
if (this.max_lifetime_interval_ms == 0) return;
if (this.max_lifetime_timer.state == .ACTIVE) return;
this.max_lifetime_timer.next = bun.timespec.msFromNow(@intCast(this.max_lifetime_interval_ms));
this.#vm.timer.insert(&this.max_lifetime_timer);
}
pub fn constructor(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*@This() {
_ = callframe;
return globalObject.throw("MySQLConnection cannot be constructed directly", .{});
}
pub fn enqueueRequest(this: *@This(), item: *JSMySQLQuery) void {
debug("enqueueRequest", .{});
this.#connection.enqueueRequest(item);
this.resetConnectionTimeout();
this.registerAutoFlusher();
}
pub fn close(this: *@This()) void {
this.ref();
this.stopTimers();
this.unregisterAutoFlusher();
defer {
this.updateReferenceType();
this.deref();
}
if (this.#vm.isShuttingDown()) {
this.#connection.close();
} else {
this.#connection.cleanQueueAndClose(null, this.getQueriesArray());
}
}
fn drainInternal(this: *@This()) void {
if (this.#vm.isShuttingDown()) return this.close();
this.ref();
defer this.deref();
const event_loop = this.#vm.eventLoop();
event_loop.enter();
defer event_loop.exit();
this.ensureJSValueIsAlive();
this.#connection.flushQueue() catch |err| {
bun.assert_eql(err, error.AuthenticationFailed);
this.fail("Authentication failed", err);
return;
};
}
pub fn deinit(this: *@This()) void {
this.stopTimers();
this.#poll_ref.unref(this.#vm);
this.unregisterAutoFlusher();
this.#connection.cleanup();
bun.destroy(this);
}
fn ensureJSValueIsAlive(this: *@This()) void {
if (this.#js_value.tryGet()) |value| {
value.ensureStillAlive();
}
}
pub fn finalize(this: *@This()) void {
debug("finalize", .{});
this.#js_value.finalize();
this.deref();
}
fn SocketHandler(comptime ssl: bool) type {
return struct {
const SocketType = uws.NewSocketHandler(ssl);
fn _socket(s: SocketType) uws.AnySocket {
if (comptime ssl) {
return uws.AnySocket{ .SocketTLS = s };
}
return uws.AnySocket{ .SocketTCP = s };
}
pub fn onOpen(this: *JSMySQLConnection, s: SocketType) void {
const socket = _socket(s);
this.#connection.setSocket(socket);
this.setupMaxLifetimeTimerIfNecessary();
this.resetConnectionTimeout();
if (socket == .SocketTCP) {
// when upgrading to TLS the onOpen callback will be called again and at this moment we dont wanna to change the status to handshaking
this.#connection.status = .handshaking;
this.ref(); // keep a ref for the socket
}
this.updateReferenceType();
}
fn onHandshake_(
this: *JSMySQLConnection,
_: anytype,
success: i32,
ssl_error: uws.us_bun_verify_error_t,
) void {
const handshakeWasSuccessful = this.#connection.doHandshake(success, ssl_error) catch |err| return this.failFmt(err, "Failed to send handshake response", .{});
if (!handshakeWasSuccessful) {
this.failWithJSValue(ssl_error.toJS(this.#globalObject));
}
}
pub const onHandshake = if (ssl) onHandshake_ else null;
pub fn onClose(this: *JSMySQLConnection, _: SocketType, _: i32, _: ?*anyopaque) void {
defer this.deref();
this.fail("Connection closed", error.ConnectionClosed);
}
pub fn onEnd(_: *JSMySQLConnection, socket: SocketType) void {
// no half closed sockets
socket.close(.normal);
}
pub fn onConnectError(this: *JSMySQLConnection, _: SocketType, _: i32) void {
// TODO: proper propagation of the error
this.fail("Connection closed", error.ConnectionClosed);
}
pub fn onTimeout(this: *JSMySQLConnection, _: SocketType) void {
this.fail("Connection timeout", error.ConnectionTimedOut);
}
pub fn onData(this: *JSMySQLConnection, _: SocketType, data: []const u8) void {
this.ref();
defer this.deref();
const vm = this.#vm;
defer {
// reset the connection timeout after we're done processing the data
this.resetConnectionTimeout();
this.updateReferenceType();
this.registerAutoFlusher();
}
if (this.#vm.isShuttingDown()) {
// we are shutting down lets not process the data
return;
}
const event_loop = vm.eventLoop();
event_loop.enter();
defer event_loop.exit();
this.ensureJSValueIsAlive();
this.#connection.readAndProcessData(data) catch |err| {
this.onError(null, err);
};
}
pub fn onWritable(this: *JSMySQLConnection, _: SocketType) void {
this.#connection.resetBackpressure();
this.drainInternal();
}
};
}
fn updateReferenceType(this: *@This()) void {
if (this.#js_value.isNotEmpty()) {
if (this.#connection.isActive()) {
if (this.#js_value == .weak) {
this.#js_value.upgrade(this.#globalObject);
this.#poll_ref.ref(this.#vm);
}
return;
}
if (this.#js_value == .strong) {
this.#js_value.downgrade();
this.#poll_ref.unref(this.#vm);
return;
}
}
this.#poll_ref.unref(this.#vm);
}
pub fn createInstance(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
var vm = globalObject.bunVM();
const arguments = callframe.arguments();
const hostname_str = try arguments[0].toBunString(globalObject);
defer hostname_str.deref();
const port = try arguments[1].coerce(i32, globalObject);
const username_str = try arguments[2].toBunString(globalObject);
defer username_str.deref();
const password_str = try arguments[3].toBunString(globalObject);
defer password_str.deref();
const database_str = try arguments[4].toBunString(globalObject);
defer database_str.deref();
// TODO: update this to match MySQL.
const ssl_mode: SSLMode = switch (arguments[5].toInt32()) {
0 => .disable,
1 => .prefer,
2 => .require,
3 => .verify_ca,
4 => .verify_full,
else => .disable,
};
const tls_object = arguments[6];
var tls_config: jsc.API.ServerConfig.SSLConfig = .{};
var tls_ctx: ?*uws.SocketContext = null;
if (ssl_mode != .disable) {
tls_config = if (tls_object.isBoolean() and tls_object.toBoolean())
.{}
else if (tls_object.isObject())
(jsc.API.ServerConfig.SSLConfig.fromJS(vm, globalObject, tls_object) catch return .zero) orelse .{}
else {
return globalObject.throwInvalidArguments("tls must be a boolean or an object", .{});
};
if (globalObject.hasException()) {
tls_config.deinit();
return .zero;
}
// we always request the cert so we can verify it and also we manually abort the connection if the hostname doesn't match
const original_reject_unauthorized = tls_config.reject_unauthorized;
tls_config.reject_unauthorized = 0;
tls_config.request_cert = 1;
// We create it right here so we can throw errors early.
const context_options = tls_config.asUSockets();
var err: uws.create_bun_socket_error_t = .none;
tls_ctx = uws.SocketContext.createSSLContext(vm.uwsLoop(), @sizeOf(*@This()), context_options, &err) orelse {
if (err != .none) {
return globalObject.throw("failed to create TLS context", .{});
} else {
return globalObject.throwValue(err.toJS(globalObject));
}
};
// restore the original reject_unauthorized
tls_config.reject_unauthorized = original_reject_unauthorized;
if (err != .none) {
tls_config.deinit();
if (tls_ctx) |ctx| {
ctx.deinit(true);
}
return globalObject.throwValue(err.toJS(globalObject));
}
uws.NewSocketHandler(true).configure(tls_ctx.?, true, *@This(), SocketHandler(true));
}
var username: []const u8 = "";
var password: []const u8 = "";
var database: []const u8 = "";
var options: []const u8 = "";
var path: []const u8 = "";
const options_str = try arguments[7].toBunString(globalObject);
defer options_str.deref();
const path_str = try arguments[8].toBunString(globalObject);
defer path_str.deref();
const options_buf: []u8 = brk: {
var b = bun.StringBuilder{};
b.cap += username_str.utf8ByteLength() + 1 + password_str.utf8ByteLength() + 1 + database_str.utf8ByteLength() + 1 + options_str.utf8ByteLength() + 1 + path_str.utf8ByteLength() + 1;
b.allocate(bun.default_allocator) catch {};
var u = username_str.toUTF8WithoutRef(bun.default_allocator);
defer u.deinit();
username = b.append(u.slice());
var p = password_str.toUTF8WithoutRef(bun.default_allocator);
defer p.deinit();
password = b.append(p.slice());
var d = database_str.toUTF8WithoutRef(bun.default_allocator);
defer d.deinit();
database = b.append(d.slice());
var o = options_str.toUTF8WithoutRef(bun.default_allocator);
defer o.deinit();
options = b.append(o.slice());
var _path = path_str.toUTF8WithoutRef(bun.default_allocator);
defer _path.deinit();
path = b.append(_path.slice());
break :brk b.allocatedSlice();
};
const on_connect = arguments[9];
const on_close = arguments[10];
const idle_timeout = arguments[11].toInt32();
const connection_timeout = arguments[12].toInt32();
const max_lifetime = arguments[13].toInt32();
const use_unnamed_prepared_statements = arguments[14].asBoolean();
// MySQL doesn't support unnamed prepared statements
_ = use_unnamed_prepared_statements;
var ptr = bun.new(JSMySQLConnection, .{
.#globalObject = globalObject,
.#vm = vm,
.idle_timeout_interval_ms = @intCast(idle_timeout),
.connection_timeout_ms = @intCast(connection_timeout),
.max_lifetime_interval_ms = @intCast(max_lifetime),
.#connection = MySQLConnection.init(
database,
username,
password,
options,
options_buf,
tls_config,
tls_ctx,
ssl_mode,
),
});
{
const hostname = hostname_str.toUTF8(bun.default_allocator);
defer hostname.deinit();
const ctx = vm.rareData().mysql_context.tcp orelse brk: {
const ctx_ = uws.SocketContext.createNoSSLContext(vm.uwsLoop(), @sizeOf(*@This())).?;
uws.NewSocketHandler(false).configure(ctx_, true, *@This(), SocketHandler(false));
vm.rareData().mysql_context.tcp = ctx_;
break :brk ctx_;
};
if (path.len > 0) {
ptr.#connection.setSocket(.{
.SocketTCP = uws.SocketTCP.connectUnixAnon(path, ctx, ptr, false) catch |err| {
ptr.deref();
return globalObject.throwError(err, "failed to connect to postgresql");
},
});
} else {
ptr.#connection.setSocket(.{
.SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| {
ptr.deref();
return globalObject.throwError(err, "failed to connect to mysql");
},
});
}
}
ptr.#connection.status = .connecting;
ptr.resetConnectionTimeout();
ptr.#poll_ref.ref(vm);
const js_value = ptr.toJS(globalObject);
js_value.ensureStillAlive();
ptr.#js_value.setStrong(js_value, globalObject);
js.onconnectSetCached(js_value, globalObject, on_connect);
js.oncloseSetCached(js_value, globalObject, on_close);
return js_value;
}
pub fn getQueries(_: *@This(), thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
if (js.queriesGetCached(thisValue)) |value| {
return value;
}
const array = try jsc.JSValue.createEmptyArray(globalObject, 0);
js.queriesSetCached(thisValue, globalObject, array);
return array;
}
pub fn getConnected(this: *@This(), _: *jsc.JSGlobalObject) JSValue {
return JSValue.jsBoolean(this.#connection.status == .connected);
}
pub fn getOnConnect(_: *@This(), thisValue: jsc.JSValue, _: *jsc.JSGlobalObject) jsc.JSValue {
if (js.onconnectGetCached(thisValue)) |value| {
return value;
}
return .js_undefined;
}
pub fn setOnConnect(_: *@This(), thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject, value: jsc.JSValue) void {
js.onconnectSetCached(thisValue, globalObject, value);
}
pub fn getOnClose(_: *@This(), thisValue: jsc.JSValue, _: *jsc.JSGlobalObject) jsc.JSValue {
if (js.oncloseGetCached(thisValue)) |value| {
return value;
}
return .js_undefined;
}
pub fn setOnClose(_: *@This(), thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject, value: jsc.JSValue) void {
js.oncloseSetCached(thisValue, globalObject, value);
}
pub fn doRef(this: *@This(), _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
this.#poll_ref.ref(this.#vm);
return .js_undefined;
}
pub fn doUnref(this: *@This(), _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
this.#poll_ref.unref(this.#vm);
return .js_undefined;
}
pub fn doFlush(this: *@This(), _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
this.registerAutoFlusher();
return .js_undefined;
}
pub fn doClose(this: *@This(), globalObject: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
_ = globalObject;
this.stopTimers();
defer this.updateReferenceType();
this.#connection.cleanQueueAndClose(null, this.getQueriesArray());
return .js_undefined;
}
fn consumeOnConnectCallback(this: *const @This(), globalObject: *jsc.JSGlobalObject) ?jsc.JSValue {
if (this.#vm.isShuttingDown()) return null;
if (this.#js_value.tryGet()) |value| {
const on_connect = js.onconnectGetCached(value) orelse return null;
js.onconnectSetCached(value, globalObject, .zero);
return on_connect;
}
return null;
}
fn consumeOnCloseCallback(this: *const @This(), globalObject: *jsc.JSGlobalObject) ?jsc.JSValue {
if (this.#vm.isShuttingDown()) return null;
if (this.#js_value.tryGet()) |value| {
const on_close = js.oncloseGetCached(value) orelse return null;
js.oncloseSetCached(value, globalObject, .zero);
return on_close;
}
return null;
}
pub fn getQueriesArray(this: *@This()) JSValue {
if (this.#vm.isShuttingDown()) return .js_undefined;
if (this.#js_value.tryGet()) |value| {
return js.queriesGetCached(value) orelse .js_undefined;
}
return .js_undefined;
}
pub inline fn isAbleToWrite(this: *@This()) bool {
return this.#connection.isAbleToWrite();
}
pub inline fn isConnected(this: *@This()) bool {
return this.#connection.status == .connected;
}
pub inline fn canPipeline(this: *@This()) bool {
return this.#connection.canPipeline();
}
pub inline fn canPrepareQuery(this: *@This()) bool {
return this.#connection.canPrepareQuery();
}
pub inline fn canExecuteQuery(this: *@This()) bool {
return this.#connection.canExecuteQuery();
}
pub inline fn getWriter(this: *@This()) NewWriter(MySQLConnection.Writer) {
return this.#connection.writer();
}
fn failFmt(this: *@This(), error_code: AnyMySQLError.Error, comptime fmt: [:0]const u8, args: anytype) void {
const message = bun.handleOom(std.fmt.allocPrint(bun.default_allocator, fmt, args));
defer bun.default_allocator.free(message);
const err = AnyMySQLError.mysqlErrorToJS(this.#globalObject, message, error_code);
this.failWithJSValue(err);
}
fn failWithJSValue(this: *@This(), value: JSValue) void {
this.ref();
defer {
if (this.#vm.isShuttingDown()) {
this.#connection.close();
} else {
this.#connection.cleanQueueAndClose(value, this.getQueriesArray());
}
this.updateReferenceType();
this.deref();
}
this.stopTimers();
if (this.#connection.status == .failed) return;
this.#connection.status = .failed;
if (this.#vm.isShuttingDown()) return;
const on_close = this.consumeOnCloseCallback(this.#globalObject) orelse return;
on_close.ensureStillAlive();
const loop = this.#vm.eventLoop();
// loop.enter();
// defer loop.exit();
this.ensureJSValueIsAlive();
var js_error = value.toError() orelse value;
if (js_error == .zero) {
js_error = AnyMySQLError.mysqlErrorToJS(this.#globalObject, "Connection closed", error.ConnectionClosed);
}
js_error.ensureStillAlive();
const queries_array = this.getQueriesArray();
queries_array.ensureStillAlive();
// this.#globalObject.queueMicrotask(on_close, &[_]JSValue{ js_error, queries_array });
loop.runCallback(on_close, this.#globalObject, .js_undefined, &[_]JSValue{ js_error, queries_array });
}
fn fail(this: *@This(), message: []const u8, err: AnyMySQLError.Error) void {
const instance = AnyMySQLError.mysqlErrorToJS(this.#globalObject, message, err);
this.failWithJSValue(instance);
}
pub fn onConnectionEstabilished(this: *@This()) void {
if (this.#vm.isShuttingDown()) return;
const on_connect = this.consumeOnConnectCallback(this.#globalObject) orelse return;
on_connect.ensureStillAlive();
var js_value = this.#js_value.tryGet() orelse .js_undefined;
js_value.ensureStillAlive();
// this.#globalObject.queueMicrotask(on_connect, &[_]JSValue{ JSValue.jsNull(), js_value });
const loop = this.#vm.eventLoop();
loop.runCallback(on_connect, this.#globalObject, .js_undefined, &[_]JSValue{ JSValue.jsNull(), js_value });
this.#poll_ref.unref(this.#vm);
}
pub fn onQueryResult(this: *@This(), request: *JSMySQLQuery, result: MySQLQueryResult) void {
request.resolve(this.getQueriesArray(), result);
}
pub fn onResultRow(this: *@This(), request: *JSMySQLQuery, statement: *MySQLStatement, Context: type, reader: NewReader(Context)) error{ShortRead}!void {
const result_mode = request.getResultMode();
var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator);
const allocator = stack_fallback.get();
var row = ResultSet.Row{
.globalObject = this.#globalObject,
.columns = statement.columns,
.binary = !request.isSimple(),
.raw = result_mode == .raw,
.bigint = request.isBigintSupported(),
};
var structure: JSValue = .js_undefined;
var cached_structure: ?CachedStructure = null;
switch (result_mode) {
.objects => {
cached_structure = if (this.#js_value.tryGet()) |value| statement.structure(value, this.#globalObject) else null;
structure = cached_structure.?.jsValue() orelse .js_undefined;
},
.raw, .values => {
// no need to check for duplicate fields or structure
},
}
defer row.deinit(allocator);
row.decode(allocator, reader) catch |err| {
if (err == error.ShortRead) {
return error.ShortRead;
}
this.#connection.queue.markCurrentRequestAsFinished(request);
request.reject(this.getQueriesArray(), err);
return;
};
const pending_value = request.getPendingValue() orelse .js_undefined;
// Process row data
const row_value = row.toJS(
this.#globalObject,
pending_value,
structure,
statement.fields_flags,
result_mode,
cached_structure,
);
if (this.#globalObject.tryTakeException()) |err| {
this.#connection.queue.markCurrentRequestAsFinished(request);
request.rejectWithJSValue(this.getQueriesArray(), err);
return;
}
statement.result_count += 1;
if (pending_value.isEmptyOrUndefinedOrNull()) {
request.setPendingValue(row_value);
}
}
pub fn onError(this: *@This(), request: ?*JSMySQLQuery, err: AnyMySQLError.Error) void {
if (request) |_request| {
if (this.#vm.isShuttingDown()) {
_request.markAsFailed();
return;
}
if (this.#globalObject.tryTakeException()) |err_| {
_request.rejectWithJSValue(this.getQueriesArray(), err_);
} else {
_request.reject(this.getQueriesArray(), err);
}
} else {
if (this.#vm.isShuttingDown()) {
this.close();
return;
}
if (this.#globalObject.tryTakeException()) |err_| {
this.failWithJSValue(err_);
} else {
this.fail("Connection closed", err);
}
}
}
pub fn onErrorPacket(
this: *@This(),
request: ?*JSMySQLQuery,
err: ErrorPacket,
) void {
if (request) |_request| {
if (this.#vm.isShuttingDown()) {
_request.markAsFailed();
} else {
if (this.#globalObject.tryTakeException()) |err_| {
_request.rejectWithJSValue(this.getQueriesArray(), err_);
} else {
_request.rejectWithJSValue(this.getQueriesArray(), err.toJS(this.#globalObject));
}
}
} else {
if (this.#vm.isShuttingDown()) {
this.close();
return;
}
if (this.#globalObject.tryTakeException()) |err_| {
this.failWithJSValue(err_);
} else {
this.failWithJSValue(err.toJS(this.#globalObject));
}
}
}
pub fn getStatementFromSignatureHash(this: *@This(), signature_hash: u64) !MySQLConnection.PreparedStatementsMapGetOrPutResult {
return try this.#connection.statements.getOrPut(bun.default_allocator, signature_hash);
}
const RefCount = bun.ptr.RefCount(@This(), "__ref_count", deinit, .{});
pub const js = jsc.Codegen.JSMySQLConnection;
pub const fromJS = js.fromJS;
pub const fromJSDirect = js.fromJSDirect;
pub const toJS = js.toJS;
pub const Writer = MySQLConnection.Writer;
const debug = bun.Output.scoped(.MySQLConnection, .visible);
const AnyMySQLError = @import("../protocol/AnyMySQLError.zig");
const CachedStructure = @import("../../shared/CachedStructure.zig");
const ErrorPacket = @import("../protocol/ErrorPacket.zig");
const JSMySQLQuery = @import("./JSMySQLQuery.zig");
const MySQLConnection = @import("../MySQLConnection.zig");
const MySQLQueryResult = @import("../MySQLQueryResult.zig");
const MySQLStatement = @import("../MySQLStatement.zig");
const ResultSet = @import("../protocol/ResultSet.zig");
const std = @import("std");
const NewReader = @import("../protocol/NewReader.zig").NewReader;
const NewWriter = @import("../protocol/NewWriter.zig").NewWriter;
const SSLMode = @import("../SSLMode.zig").SSLMode;
const bun = @import("bun");
const uws = bun.uws;
const jsc = bun.jsc;
const JSGlobalObject = jsc.JSGlobalObject;
const JSValue = jsc.JSValue;
const AutoFlusher = jsc.WebCore.AutoFlusher;

View File

@@ -0,0 +1,402 @@
const JSMySQLQuery = @This();
const RefCount = bun.ptr.RefCount(@This(), "__ref_count", deinit, .{});
#thisValue: JSRef = JSRef.empty(),
// unfortunally we cannot use #ref_count here
__ref_count: RefCount = RefCount.init(),
#vm: *jsc.VirtualMachine,
#globalObject: *jsc.JSGlobalObject,
#query: MySQLQuery,
pub const ref = RefCount.ref;
pub const deref = RefCount.deref;
pub fn estimatedSize(this: *@This()) usize {
_ = this;
return @sizeOf(@This());
}
pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*@This() {
_ = callframe;
return globalThis.throwInvalidArguments("MySQLQuery cannot be constructed directly", .{});
}
fn deinit(this: *@This()) void {
this.#query.cleanup();
bun.destroy(this);
}
pub fn finalize(this: *@This()) void {
debug("MySQLQuery finalize", .{});
this.#thisValue.finalize();
this.deref();
}
pub fn createInstance(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments();
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
const query = args.nextEat() orelse {
return globalThis.throw("query must be a string", .{});
};
const values = args.nextEat() orelse {
return globalThis.throw("values must be an array", .{});
};
if (!query.isString()) {
return globalThis.throw("query must be a string", .{});
}
if (values.jsType() != .Array) {
return globalThis.throw("values must be an array", .{});
}
const pending_value: JSValue = args.nextEat() orelse .js_undefined;
const columns: JSValue = args.nextEat() orelse .js_undefined;
const js_bigint: JSValue = args.nextEat() orelse .false;
const js_simple: JSValue = args.nextEat() orelse .false;
const bigint = js_bigint.isBoolean() and js_bigint.asBoolean();
const simple = js_simple.isBoolean() and js_simple.asBoolean();
if (simple) {
if (try values.getLength(globalThis) > 0) {
return globalThis.throwInvalidArguments("simple query cannot have parameters", .{});
}
if (try query.getLength(globalThis) >= std.math.maxInt(i32)) {
return globalThis.throwInvalidArguments("query is too long", .{});
}
}
if (!pending_value.jsType().isArrayLike()) {
return globalThis.throwInvalidArgumentType("query", "pendingValue", "Array");
}
var this = bun.new(@This(), .{
.#query = MySQLQuery.init(
try query.toBunString(globalThis),
bigint,
simple,
),
.#globalObject = globalThis,
.#vm = globalThis.bunVM(),
});
const this_value = this.toJS(globalThis);
this_value.ensureStillAlive();
this.#thisValue.setWeak(this_value);
this.setBinding(values);
this.setPendingValue(pending_value);
if (!columns.isUndefined()) {
this.setColumns(columns);
}
return this_value;
}
pub fn doRun(this: *@This(), globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
debug("doRun", .{});
this.ref();
defer this.deref();
var arguments = callframe.arguments();
if (arguments.len < 2) {
return globalObject.throwInvalidArguments("run must be called with 2 arguments connection and target", .{});
}
const connection: *MySQLConnection = arguments[0].as(MySQLConnection) orelse {
return globalObject.throw("connection must be a MySQLConnection", .{});
};
var target = arguments[1];
if (!target.isObject()) {
return globalObject.throwInvalidArgumentType("run", "query", "Query");
}
this.setTarget(target);
this.run(connection) catch |err| {
if (!globalObject.hasException()) {
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to execute query", err));
}
return error.JSError;
};
connection.enqueueRequest(this);
return .js_undefined;
}
pub fn doCancel(_: *@This(), _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
// TODO: we can cancel a query that is pending aka not pipelined yet we just need fail it
// if is running is not worth/viable to cancel the whole connection
return .js_undefined;
}
pub fn doDone(_: *@This(), _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
// TODO: investigate why this function is needed
return .js_undefined;
}
pub fn setModeFromJS(this: *@This(), globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const js_mode = callframe.argument(0);
if (js_mode.isEmptyOrUndefinedOrNull() or !js_mode.isNumber()) {
return globalObject.throwInvalidArgumentType("setMode", "mode", "Number");
}
const mode_value = try js_mode.coerce(i32, globalObject);
const mode = std.meta.intToEnum(SQLQueryResultMode, mode_value) catch {
return globalObject.throwInvalidArgumentTypeValue("mode", "Number", js_mode);
};
this.#query.setResultMode(mode);
return .js_undefined;
}
pub fn setPendingValueFromJS(this: *@This(), _: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const result = callframe.argument(0);
this.setPendingValue(result);
return .js_undefined;
}
pub fn resolve(
this: *@This(),
queries_array: JSValue,
result: MySQLQueryResult,
) void {
this.ref();
const is_last_result = result.is_last_result;
defer {
if (this.#thisValue.isNotEmpty() and is_last_result) {
this.#thisValue.downgrade();
}
this.deref();
}
if (!this.#query.result(is_last_result)) {
return;
}
if (this.#vm.isShuttingDown()) {
return;
}
const targetValue = this.getTarget() orelse return;
const thisValue = this.#thisValue.tryGet() orelse return;
thisValue.ensureStillAlive();
const tag: CommandTag = .{ .SELECT = result.result_count };
const js_tag = tag.toJSTag(this.#globalObject) catch return bun.assertf(false, "in MySQLQuery Tag should always be a number", .{});
js_tag.ensureStillAlive();
const function = this.#vm.rareData().mysql_context.onQueryResolveFn.get() orelse return;
bun.assertf(function.isCallable(), "onQueryResolveFn is not callable", .{});
const event_loop = this.#vm.eventLoop();
const pending_value = this.getPendingValue() orelse .js_undefined;
pending_value.ensureStillAlive();
this.setPendingValue(.js_undefined);
event_loop.runCallback(function, this.#globalObject, thisValue, &.{
targetValue,
pending_value,
js_tag,
tag.toJSNumber(),
if (queries_array == .zero) .js_undefined else queries_array,
JSValue.jsBoolean(is_last_result),
JSValue.jsNumber(result.last_insert_id),
JSValue.jsNumber(result.affected_rows),
});
}
pub fn markAsFailed(this: *@This()) void {
// Attention: we cannot touch JS here
// If you need to touch JS, you wanna to use reject or rejectWithJSValue instead
this.ref();
defer this.deref();
if (this.#thisValue.isNotEmpty()) {
this.#thisValue.downgrade();
}
_ = this.#query.fail();
}
pub fn reject(this: *@This(), queries_array: JSValue, err: AnyMySQLError.Error) void {
if (this.#vm.isShuttingDown()) {
this.markAsFailed();
return;
}
if (this.#globalObject.tryTakeException()) |err_| {
this.rejectWithJSValue(queries_array, err_);
} else {
const instance = AnyMySQLError.mysqlErrorToJS(this.#globalObject, "Failed to bind query", err);
instance.ensureStillAlive();
this.rejectWithJSValue(queries_array, instance);
}
}
pub fn rejectWithJSValue(this: *@This(), queries_array: JSValue, err: JSValue) void {
this.ref();
defer {
if (this.#thisValue.isNotEmpty()) {
this.#thisValue.downgrade();
}
this.deref();
}
if (!this.#query.fail()) {
return;
}
if (this.#vm.isShuttingDown()) {
return;
}
const targetValue = this.getTarget() orelse return;
var js_error = err.toError() orelse err;
if (js_error == .zero) {
js_error = AnyMySQLError.mysqlErrorToJS(this.#globalObject, "Query failed", error.UnknownError);
}
bun.assertf(js_error != .zero, "js_error is zero", .{});
js_error.ensureStillAlive();
const function = this.#vm.rareData().mysql_context.onQueryRejectFn.get() orelse return;
bun.assertf(function.isCallable(), "onQueryRejectFn is not callable", .{});
const event_loop = this.#vm.eventLoop();
const js_array = if (queries_array == .zero) .js_undefined else queries_array;
js_array.ensureStillAlive();
event_loop.runCallback(function, this.#globalObject, this.#thisValue.tryGet() orelse return, &.{
targetValue,
js_error,
js_array,
});
}
pub fn run(this: *@This(), connection: *MySQLConnection) AnyMySQLError.Error!void {
if (this.#vm.isShuttingDown()) {
debug("run cannot run a query if the VM is shutting down", .{});
// cannot run a query if the VM is shutting down
return;
}
if (!this.#query.isPending() or this.#query.isBeingPrepared()) {
debug("run already running or being prepared", .{});
// already running or completed
return;
}
const globalObject = this.#globalObject;
this.#thisValue.upgrade(globalObject);
errdefer {
this.#thisValue.downgrade();
_ = this.#query.fail();
}
const columns_value = this.getColumns() orelse .js_undefined;
const binding_value = this.getBinding() orelse .js_undefined;
this.#query.runQuery(connection, globalObject, columns_value, binding_value) catch |err| {
debug("run failed to execute query", .{});
if (!globalObject.hasException())
return globalObject.throwValue(AnyMySQLError.mysqlErrorToJS(globalObject, "failed to execute query", err));
return error.JSError;
};
}
pub inline fn isCompleted(this: *@This()) bool {
return this.#query.isCompleted();
}
pub inline fn isRunning(this: *@This()) bool {
return this.#query.isRunning();
}
pub inline fn isPending(this: *@This()) bool {
return this.#query.isPending();
}
pub inline fn isBeingPrepared(this: *@This()) bool {
return this.#query.isBeingPrepared();
}
pub inline fn isPipelined(this: *@This()) bool {
return this.#query.isPipelined();
}
pub inline fn isSimple(this: *@This()) bool {
return this.#query.isSimple();
}
pub inline fn isBigintSupported(this: *@This()) bool {
return this.#query.isBigintSupported();
}
pub inline fn getResultMode(this: *@This()) SQLQueryResultMode {
return this.#query.getResultMode();
}
// TODO: isolate statement modification away from the connection
pub fn getStatement(this: *@This()) ?*MySQLStatement {
return this.#query.getStatement();
}
pub fn markAsPrepared(this: *@This()) void {
this.#query.markAsPrepared();
}
pub inline fn setPendingValue(this: *@This(), result: JSValue) void {
if (this.#vm.isShuttingDown()) return;
if (this.#thisValue.tryGet()) |value| {
js.pendingValueSetCached(value, this.#globalObject, result);
}
}
pub inline fn getPendingValue(this: *@This()) ?JSValue {
if (this.#vm.isShuttingDown()) return null;
if (this.#thisValue.tryGet()) |value| {
return js.pendingValueGetCached(value);
}
return null;
}
inline fn setTarget(this: *@This(), result: JSValue) void {
if (this.#vm.isShuttingDown()) return;
if (this.#thisValue.tryGet()) |value| {
js.targetSetCached(value, this.#globalObject, result);
}
}
inline fn getTarget(this: *@This()) ?JSValue {
if (this.#vm.isShuttingDown()) return null;
if (this.#thisValue.tryGet()) |value| {
return js.targetGetCached(value);
}
return null;
}
inline fn setColumns(this: *@This(), result: JSValue) void {
if (this.#vm.isShuttingDown()) return;
if (this.#thisValue.tryGet()) |value| {
js.columnsSetCached(value, this.#globalObject, result);
}
}
inline fn getColumns(this: *@This()) ?JSValue {
if (this.#vm.isShuttingDown()) return null;
if (this.#thisValue.tryGet()) |value| {
return js.columnsGetCached(value);
}
return null;
}
inline fn setBinding(this: *@This(), result: JSValue) void {
if (this.#vm.isShuttingDown()) return;
if (this.#thisValue.tryGet()) |value| {
js.bindingSetCached(value, this.#globalObject, result);
}
}
inline fn getBinding(this: *@This()) ?JSValue {
if (this.#vm.isShuttingDown()) return null;
if (this.#thisValue.tryGet()) |value| {
return js.bindingGetCached(value);
}
return null;
}
comptime {
@export(&jsc.toJSHostFn(createInstance), .{ .name = "MySQLQuery__createInstance" });
}
pub const js = jsc.Codegen.JSMySQLQuery;
pub const fromJS = js.fromJS;
pub const fromJSDirect = js.fromJSDirect;
pub const toJS = js.toJS;
const debug = bun.Output.scoped(.MySQLQuery, .visible);
const AnyMySQLError = @import("../protocol/AnyMySQLError.zig");
const MySQLConnection = @import("./JSMySQLConnection.zig");
const MySQLQuery = @import("../MySQLQuery.zig");
const MySQLQueryResult = @import("../MySQLQueryResult.zig");
const MySQLStatement = @import("../MySQLStatement.zig");
const bun = @import("bun");
const std = @import("std");
const CommandTag = @import("../../postgres/CommandTag.zig").CommandTag;
const SQLQueryResultMode = @import("../../shared/SQLQueryResultMode.zig").SQLQueryResultMode;
const jsc = bun.jsc;
const JSRef = jsc.JSRef;
const JSValue = jsc.JSValue;

View File

@@ -33,6 +33,8 @@ pub const Error = error{
InvalidErrorPacket,
UnexpectedPacket,
ShortRead,
UnknownError,
InvalidState,
};
pub fn mysqlErrorToJS(globalObject: *jsc.JSGlobalObject, message: ?[]const u8, err: Error) JSValue {
@@ -64,6 +66,8 @@ pub fn mysqlErrorToJS(globalObject: *jsc.JSGlobalObject, message: ?[]const u8, e
error.MissingAuthData => "ERR_MYSQL_MISSING_AUTH_DATA",
error.FailedToEncryptPassword => "ERR_MYSQL_FAILED_TO_ENCRYPT_PASSWORD",
error.InvalidPublicKey => "ERR_MYSQL_INVALID_PUBLIC_KEY",
error.UnknownError => "ERR_MYSQL_UNKNOWN_ERROR",
error.InvalidState => "ERR_MYSQL_INVALID_STATE",
error.JSError => {
return globalObject.takeException(error.JSError);
},

View File

@@ -19,7 +19,7 @@ pub fn createMySQLError(
message: []const u8,
options: MySQLErrorOptions,
) bun.JSError!JSValue {
const opts_obj = JSValue.createEmptyObject(globalObject, 18);
const opts_obj = JSValue.createEmptyObject(globalObject, 0);
opts_obj.ensureStillAlive();
opts_obj.put(globalObject, JSC.ZigString.static("code"), try bun.String.createUTF8ForJS(globalObject, options.code));
if (options.errno) |errno| {

View File

@@ -29,7 +29,7 @@ pub const CommandTag = union(enum) {
other: []const u8,
pub fn toJSTag(this: CommandTag, globalObject: *jsc.JSGlobalObject) JSValue {
pub fn toJSTag(this: CommandTag, globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
return switch (this) {
.INSERT => JSValue.jsNumber(1),
.DELETE => JSValue.jsNumber(2),
@@ -39,7 +39,7 @@ pub const CommandTag = union(enum) {
.MOVE => JSValue.jsNumber(6),
.FETCH => JSValue.jsNumber(7),
.COPY => JSValue.jsNumber(8),
.other => |tag| jsc.ZigString.init(tag).toJS(globalObject),
.other => |tag| bun.String.createUTF8ForJS(globalObject, tag),
};
}

View File

@@ -219,7 +219,7 @@ pub fn onConnectionTimeout(this: *PostgresSQLConnection) bun.api.Timer.EventLoop
this.failFmt("ERR_POSTGRES_CONNECTION_TIMEOUT", "Connection timeout after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)});
},
.sent_startup_message => {
this.failFmt("ERR_POSTGRES_CONNECTION_TIMEOUT", "Connection timed out after {} (sent startup message, but never received response)", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)});
this.failFmt("ERR_POSTGRES_CONNECTION_TIMEOUT", "Connection timeout after {} (sent startup message, but never received response)", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)});
},
}
return .disarm;
@@ -311,7 +311,7 @@ pub fn failWithJSValue(this: *PostgresSQLConnection, value: JSValue) void {
this.stopTimers();
if (this.status == .failed) return;
this.setStatus(.failed);
this.status = .failed;
this.ref();
defer this.deref();
@@ -321,12 +321,17 @@ pub fn failWithJSValue(this: *PostgresSQLConnection, value: JSValue) void {
const loop = this.vm.eventLoop();
loop.enter();
var js_error = value.toError() orelse value;
if (js_error == .zero) {
js_error = postgresErrorToJS(this.globalObject, "Connection closed", error.ConnectionClosed);
}
js_error.ensureStillAlive();
defer loop.exit();
_ = on_close.call(
this.globalObject,
this.js_value,
.js_undefined,
&[_]JSValue{
value.toError() orelse value,
js_error,
this.getQueriesArray(),
},
) catch |e| this.globalObject.reportActiveExceptionAsUnhandled(e);
@@ -1350,6 +1355,9 @@ fn advance(this: *PostgresSQLConnection) void {
}
pub fn getQueriesArray(this: *const PostgresSQLConnection) JSValue {
if (this.js_value.isEmptyOrUndefinedOrNull()) {
return .js_undefined;
}
return js.queriesGetCached(this.js_value) orelse .js_undefined;
}

View File

@@ -1,5 +1,5 @@
const PostgresSQLQuery = @This();
const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", deinit, .{});
const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{});
statement: ?*PostgresSQLStatement = null,
query: bun.String = bun.String.empty,
cursor_name: bun.String = bun.String.empty,
@@ -23,9 +23,9 @@ flags: packed struct(u8) {
pub const ref = RefCount.ref;
pub const deref = RefCount.deref;
pub fn getTarget(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, clean_target: bool) jsc.JSValue {
const thisValue = this.thisValue.tryGet() orelse return .zero;
const target = js.targetGetCached(thisValue) orelse return .zero;
pub fn getTarget(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, clean_target: bool) ?jsc.JSValue {
const thisValue = this.thisValue.tryGet() orelse return null;
const target = js.targetGetCached(thisValue) orelse return null;
if (clean_target) {
js.targetSetCached(thisValue, globalObject, .zero);
}
@@ -51,12 +51,7 @@ pub const Status = enum(u8) {
}
};
pub fn hasPendingActivity(this: *@This()) bool {
return this.ref_count.get() > 1;
}
pub fn deinit(this: *@This()) void {
this.thisValue.deinit();
if (this.statement) |statement| {
statement.deref();
}
@@ -67,11 +62,7 @@ pub fn deinit(this: *@This()) void {
pub fn finalize(this: *@This()) void {
debug("PostgresSQLQuery finalize", .{});
if (this.thisValue == .weak) {
// clean up if is a weak reference, if is a strong reference we need to wait until the query is done
// if we are a strong reference, here is probably a bug because GC'd should not happen
this.thisValue.weak = .zero;
}
this.thisValue.finalize();
this.deref();
}
@@ -84,12 +75,9 @@ pub fn onWriteFail(
this.ref();
defer this.deref();
this.status = .fail;
const thisValue = this.thisValue.get();
defer this.thisValue.deinit();
const targetValue = this.getTarget(globalObject, true);
if (thisValue == .zero or targetValue == .zero) {
return;
}
const thisValue = this.thisValue.tryGet() orelse return;
defer this.thisValue.downgrade();
const targetValue = this.getTarget(globalObject, true) orelse return;
const vm = jsc.VirtualMachine.get();
const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?;
@@ -105,12 +93,9 @@ pub fn onJSError(this: *@This(), err: jsc.JSValue, globalObject: *jsc.JSGlobalOb
this.ref();
defer this.deref();
this.status = .fail;
const thisValue = this.thisValue.get();
defer this.thisValue.deinit();
const targetValue = this.getTarget(globalObject, true);
if (thisValue == .zero or targetValue == .zero) {
return;
}
const thisValue = this.thisValue.tryGet() orelse return;
defer this.thisValue.downgrade();
const targetValue = this.getTarget(globalObject, true) orelse return;
var vm = jsc.VirtualMachine.get();
const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?;
@@ -145,31 +130,30 @@ fn consumePendingValue(thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject
pub fn onResult(this: *@This(), command_tag_str: []const u8, globalObject: *jsc.JSGlobalObject, connection: jsc.JSValue, is_last: bool) void {
this.ref();
defer this.deref();
const thisValue = this.thisValue.get();
const targetValue = this.getTarget(globalObject, is_last);
if (is_last) {
this.status = .success;
} else {
this.status = .partial_response;
}
const tag = CommandTag.init(command_tag_str);
const js_tag = tag.toJSTag(globalObject) catch |e| return this.onJSError(globalObject.takeException(e), globalObject);
js_tag.ensureStillAlive();
const thisValue = this.thisValue.tryGet() orelse return;
defer if (is_last) {
allowGC(thisValue, globalObject);
this.thisValue.deinit();
this.thisValue.downgrade();
};
if (thisValue == .zero or targetValue == .zero) {
return;
}
const targetValue = this.getTarget(globalObject, is_last) orelse return;
const vm = jsc.VirtualMachine.get();
const function = vm.rareData().postgresql_context.onQueryResolveFn.get().?;
const event_loop = vm.eventLoop();
const tag = CommandTag.init(command_tag_str);
event_loop.runCallback(function, globalObject, thisValue, &.{
targetValue,
consumePendingValue(thisValue, globalObject) orelse .js_undefined,
tag.toJSTag(globalObject),
js_tag,
tag.toJSNumber(),
if (connection == .zero) .js_undefined else PostgresSQLConnection.js.queriesGetCached(connection) orelse .js_undefined,
JSValue.jsBoolean(is_last),
@@ -257,13 +241,13 @@ pub fn doDone(this: *@This(), globalObject: *jsc.JSGlobalObject, _: *jsc.CallFra
this.flags.is_done = true;
return .js_undefined;
}
pub fn setPendingValue(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
pub fn setPendingValueFromJS(_: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const result = callframe.argument(0);
const thisValue = this.thisValue.tryGet() orelse return .js_undefined;
const thisValue = callframe.this();
js.pendingValueSetCached(thisValue, globalObject, result);
return .js_undefined;
}
pub fn setMode(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
pub fn setModeFromJS(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const js_mode = callframe.argument(0);
if (js_mode.isEmptyOrUndefinedOrNull() or !js_mode.isNumber()) {
return globalObject.throwInvalidArgumentType("setMode", "mode", "Number");

View File

@@ -11,6 +11,10 @@ array_length: usize = 0,
any_failed: bool = false,
pub fn next(this: *ObjectIterator) ?jsc.JSValue {
if (this.array.isEmptyOrUndefinedOrNull() or this.columns.isEmptyOrUndefinedOrNull()) {
this.any_failed = true;
return null;
}
if (this.row_i >= this.array_length) {
return null;
}