mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 21:32:05 +00:00
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:
@@ -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
@@ -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;
|
||||
|
||||
4
src/sql/mysql/MySQLQueryResult.zig
Normal file
4
src/sql/mysql/MySQLQueryResult.zig
Normal file
@@ -0,0 +1,4 @@
|
||||
result_count: u64,
|
||||
last_insert_id: u64,
|
||||
affected_rows: u64,
|
||||
is_last_result: bool,
|
||||
224
src/sql/mysql/MySQLRequestQueue.zig
Normal file
224
src/sql/mysql/MySQLRequestQueue.zig
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
18
src/sql/mysql/QueryStatus.zig
Normal file
18
src/sql/mysql/QueryStatus.zig
Normal 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);
|
||||
}
|
||||
};
|
||||
809
src/sql/mysql/js/JSMySQLConnection.zig
Normal file
809
src/sql/mysql/js/JSMySQLConnection.zig
Normal 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;
|
||||
402
src/sql/mysql/js/JSMySQLQuery.zig
Normal file
402
src/sql/mysql/js/JSMySQLQuery.zig
Normal 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;
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user