From 028e98e18da155c2d4124f10d0fadfb945cc1aa2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 02:12:44 -0800 Subject: [PATCH] lots of code --- src/bun.js/api/mysql.classes.ts | 4 +- src/bun.js/bindings/bindings.zig | 12 +++ src/bun.js/rare_data.zig | 1 + src/sql/mysql.zig | 133 +++++++++++++++++++++---------- src/sql/mysql/mysql_protocol.zig | 6 +- src/sql/postgres.zig | 2 +- src/sql/shared_sql.zig | 34 ++++++-- 7 files changed, 138 insertions(+), 54 deletions(-) diff --git a/src/bun.js/api/mysql.classes.ts b/src/bun.js/api/mysql.classes.ts index d2041d23d6..dd1a08ad88 100644 --- a/src/bun.js/api/mysql.classes.ts +++ b/src/bun.js/api/mysql.classes.ts @@ -2,7 +2,7 @@ import { define } from "../../codegen/class-definitions"; export default [ define({ - name: "MySQLSQLConnection", + name: "MySQLConnection", construct: true, finalize: true, hasPendingActivity: true, @@ -38,7 +38,7 @@ export default [ }, }), define({ - name: "MySQLSQLQuery", + name: "MySQLQuery", construct: true, finalize: true, configurable: false, diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index af62ff0b89..55f2ad8b40 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4168,6 +4168,18 @@ pub const JSValue = enum(i64) { return cppFn("coerceToDouble", .{ this, globalObject }); } + pub fn coerceToDoubleCheckingErrors( + this: JSValue, + globalObject: *JSC.JSGlobalObject, + ) f64 { + const num = this.coerceToDouble(globalObject); + if (globalObject.hasException()) { + return error.JSError; + } + + return num; + } + pub fn coerce(this: JSValue, comptime T: type, globalThis: *JSC.JSGlobalObject) T { return switch (T) { ZigString => this.getZigString(globalThis), diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 45ebac3282..d9be247a8c 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -24,6 +24,7 @@ stdin_store: ?*Blob.Store = null, stdout_store: ?*Blob.Store = null, postgresql_context: JSC.Postgres.PostgresSQLContext = .{}, +mysql_context: JSC.MySQL.MySQLContext = .{}, entropy_cache: ?*EntropyCache = null, diff --git a/src/sql/mysql.zig b/src/sql/mysql.zig index 9db85a5cd5..186b182208 100644 --- a/src/sql/mysql.zig +++ b/src/sql/mysql.zig @@ -20,6 +20,8 @@ pub const int2 = u16; pub const int3 = u24; pub const int4 = u32; pub const int8 = u64; +const Value = types.Value; +const FieldType = types.FieldType; pub const MySQLInt8 = int1; pub const MySQLInt16 = int2; @@ -815,14 +817,14 @@ pub const MySQLConnection = struct { // Read parameter definitions if any if (ok.num_params > 0) { - var params = try bun.default_allocator.alloc(types.FieldType, ok.num_params); + const params = try bun.default_allocator.alloc(types.FieldType, ok.num_params); errdefer bun.default_allocator.free(params); for (params) |*param| { var column = protocol.ColumnDefinition41{}; + defer column.deinit(); try column.decode(Context, reader); param.* = column.column_type; - column.deinit(); } statement.params = params; @@ -830,22 +832,32 @@ pub const MySQLConnection = struct { // Read column definitions if any if (ok.num_columns > 0) { - var columns = try bun.default_allocator.alloc(protocol.ColumnDefinition41, ok.num_columns); + const columns = try bun.default_allocator.alloc(protocol.ColumnDefinition41, ok.num_columns); + var consumed: u32 = 0; errdefer { - for (columns) |*column| { + for (columns[0..consumed]) |*column| { column.deinit(); } bun.default_allocator.free(columns); } - for (0..ok.num_columns) |i| { - try columns[i].decode(Context, reader); + for (columns) |*column| { + try column.decode(Context, reader); + consumed += 1; } statement.columns = columns; } - try this.executeStatement(statement, request, this.globalObject); + var execute = protocol.PreparedStatement.Execute{ + .statement_id = statement.statement_id, + .param_types = statement.params, + .iteration_count = 1, + }; + defer execute.deinit(); + try request.bind(&execute, this.globalObject); + try execute.writeInternal(Context, this.writer()); + this.flushData(); } }, @@ -854,7 +866,7 @@ pub const MySQLConnection = struct { try err.decode(Context, reader); defer err.deinit(); - if (this.requests.popOrNull()) |request| { + if (this.requests.readItem()) |request| { if (request.statement) |statement| { statement.status = .failed; statement.error_response = err; @@ -903,14 +915,14 @@ pub const MySQLConnection = struct { var header = protocol.ResultSetHeader{}; try header.decode(Context, reader); - if (this.requests.readableLength() > 0) { const request = this.requests.peekItem(0); // Read column definitions const columns = try bun.default_allocator.alloc(protocol.ColumnDefinition41, header.field_count); + var columns_read: u32 = 0; errdefer { - for (columns) |*column| { + for (columns[0..columns_read]) |*column| { column.deinit(); } bun.default_allocator.free(columns); @@ -918,8 +930,10 @@ pub const MySQLConnection = struct { for (columns) |*column| { try column.decode(Context, reader); + columns_read += 1; } -const globalThis = this.globalObject; + + const globalThis = this.globalObject; // Start reading rows while (true) { const row_first_byte = try reader.byte(); @@ -933,9 +947,9 @@ const globalThis = this.globalObject; // Update status flags and finish this.status_flags = eof.status_flags; this.is_ready_for_query = true; - this.requests.discard(1); - request.onSuccess(0, 0, this.globalObject); + + request.onSuccess(this.globalObject); break; }, @@ -950,50 +964,36 @@ const globalThis = this.globalObject; else => { var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); + const allocator = stack_fallback.get(); + // Read row data var row = protocol.ResultSet.Row{ .columns = columns, .binary = request.binary, }; - const allocator = stack_fallback.get(); - try row.decodeInternal(allocator, Context, reader); defer row.deinit(allocator); + try row.decodeInternal(allocator, Context, reader); + + const pending_value = MySQLQuery.pendingValueGetCached(request.thisValue) orelse .zero; // Process row data - // Note: You'll need to implement row processing logic - // based on your application's needs - - row.toJS(request.statement.?.structure(request.thisValue, globalThis), request. globalThis); + const row_value = row.toJS(request.statement.?.structure(request.thisValue, globalThis), pending_value, request.globalThis); + if (globalThis.hasException()) { + request.onJSError(globalThis.tryTakeException().?, globalThis); + return error.JSError; + } + if (pending_value == .zero) { + MySQLQuery.pendingValueSetCached(request.thisValue, globalThis, row_value); + } }, } } - - // Clean up columns - for (columns) |*column| { - column.deinit(); - } - bun.default_allocator.free(columns); } }, } } - pub fn executeStatement(this: *MySQLConnection, statement: *MySQLStatement, globalObject: *JSC.JSGlobalObject) !void { - var execute = protocol.PreparedStatement.Execute{ - .statement_id = statement.statement_id, - .params = values, - .param_types = statement.params, - .iteration_count = 1, - }; - defer execute.deinit(); - - const array - - try execute.writeInternal() - this.flushData(); - } - pub fn closeStatement(this: *MySQLConnection, statement: *MySQLStatement) !void { var close = protocol.PreparedStatement.Close{ .statement_id = statement.statement_id, @@ -1170,6 +1170,31 @@ pub const MySQLQuery = struct { }); } + pub fn bind(this: *MySQLQuery, execute: *protocol.PreparedStatement.Execute, globalObject: *JSC.JSGlobalObject) !void { + const binding_value = MySQLQuery.bindingGetCached(this.thisValue) orelse .zero; + const columns_value = MySQLQuery.columnsGetCached(this.thisValue) orelse .zero; + + var iter = QueryBindingIterator.init(binding_value, columns_value, globalObject); + + var i: u32 = 0; + var params = try bun.default_allocator.alloc(Data, execute.params.len); + errdefer { + for (params[0..i]) |*param| { + param.deinit(); + } + bun.default_allocator.free(params); + } + while (iter.next()) |js_value| { + const param = execute.param_types[i]; + const value = try Value.fromJS(js_value, globalObject, param, bun.default_allocator); + params[i] = try value.toData(param); + i += 1; + } + + this.status = .binding; + execute.params = params; + } + pub fn onError(this: *@This(), err: protocol.ErrorPacket, globalObject: *JSC.JSGlobalObject) void { this.status = .fail; defer { @@ -1192,7 +1217,29 @@ pub const MySQLQuery = struct { globalObject.queueMicrotask(function, &[_]JSValue{ targetValue, err.toJS(globalObject) }); } - pub fn onSuccess(this: *@This(), affected_rows: u64, last_insert_id: u64, globalObject: *JSC.JSGlobalObject) void { + pub fn onJSError(this: *@This(), exception: JSC.JSValue, globalObject: *JSC.JSGlobalObject) void { + this.status = .fail; + defer { + // Clean up statement reference on error + if (this.statement) |statement| { + statement.deref(); + this.statement = null; + } + this.deref(); + } + + const thisValue = this.thisValue; + const targetValue = this.target.trySwap() orelse JSValue.zero; + if (thisValue == .zero or targetValue == .zero) { + return; + } + + var vm = JSC.VirtualMachine.get(); + const function = vm.rareData().mysql_context.onQueryRejectFn.get().?; + globalObject.queueMicrotask(function, &[_]JSValue{ targetValue, exception.toError().? }); + } + + pub fn onSuccess(this: *@This(), globalObject: *JSC.JSGlobalObject) void { this.status = .success; defer this.deref(); @@ -1208,8 +1255,8 @@ pub const MySQLQuery = struct { event_loop.runCallback(function, globalObject, thisValue, &.{ targetValue, this.pending_value.trySwap() orelse .undefined, - JSValue.jsNumber(@floatFromInt(affected_rows)), - JSValue.jsNumber(@floatFromInt(last_insert_id)), + JSValue.jsNumber(0), + JSValue.jsNumber(0), }); } diff --git a/src/sql/mysql/mysql_protocol.zig b/src/sql/mysql/mysql_protocol.zig index c4c0c5f3dd..f5b6e08993 100644 --- a/src/sql/mysql/mysql_protocol.zig +++ b/src/sql/mysql/mysql_protocol.zig @@ -1486,6 +1486,8 @@ pub const PreparedStatement = struct { for (this.params, 0..) |param, i| { if (param == .empty) { null_bitmap[i >> 3] |= @as(u8, 1) << @as(u3, @truncate(i & 7)); + } else { + bun.assert(param.slice().len > 0); } } @@ -1503,8 +1505,8 @@ pub const PreparedStatement = struct { } // Write parameter values - for (this.params, this.param_types) |param, param_type| { - if (param == .empty) continue; + for (this.params, this.param_types) |*param, param_type| { + if (param.* == .empty) continue; const value = param.slice(); if (param_type.isBinaryFormatSupported()) { diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 06b189178c..c624285b74 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -1617,7 +1617,7 @@ pub const PostgresSQLConnection = struct { pub const Value = extern union { null: u8, - string: bun.WTF.StringImpl, + string: ?bun.WTF.StringImpl, float8: f64, int4: i32, int8: i64, diff --git a/src/sql/shared_sql.zig b/src/sql/shared_sql.zig index b2a44633ee..f0e6334983 100644 --- a/src/sql/shared_sql.zig +++ b/src/sql/shared_sql.zig @@ -1,6 +1,7 @@ const JSC = bun.JSC; const bun = @import("root").bun; const JSValue = JSC.JSValue; +const std = @import("std"); pub const QueryBindingIterator = union(enum) { array: JSC.JSArrayIterator, @@ -124,13 +125,29 @@ pub const QueryBindingIterator = union(enum) { pub const Data = union(enum) { owned: bun.ByteList, temporary: []const u8, + inline_storage: std.BoundedArray(u8, 15), empty: void, + pub fn create(possibly_inline_bytes: []const u8, allocator: std.mem.Allocator) !Data { + if (possibly_inline_bytes.len == 0) { + return .{ .empty = {} }; + } + + if (possibly_inline_bytes.len <= 15) { + var inline_storage = std.BoundedArray(u8, 15){}; + @memcpy(inline_storage.buffer[0..possibly_inline_bytes.len], possibly_inline_bytes); + inline_storage.len = @truncate(possibly_inline_bytes.len); + return .{ .inline_storage = inline_storage }; + } + return .{ .owned = bun.ByteList.init(try allocator.dupe(u8, possibly_inline_bytes)) }; + } + pub fn toOwned(this: @This()) !bun.ByteList { return switch (this) { .owned => this.owned, .temporary => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.temporary)), .empty => bun.ByteList.init(&.{}), + .inline_storage => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.inline_storage.slice())), }; } @@ -139,6 +156,7 @@ pub const Data = union(enum) { .owned => this.owned.deinitWithAllocator(bun.default_allocator), .temporary => {}, .empty => {}, + .inline_storage => {}, } } @@ -155,30 +173,34 @@ pub const Data = union(enum) { }, .temporary => {}, .empty => {}, + .inline_storage => {}, } } - pub fn slice(this: @This()) []const u8 { - return switch (this) { + pub fn slice(this: *const @This()) []const u8 { + return switch (this.*) { .owned => this.owned.slice(), .temporary => this.temporary, .empty => "", + .inline_storage => this.inline_storage.slice(), }; } - pub fn substring(this: @This(), start_index: usize, end_index: usize) Data { - return switch (this) { + pub fn substring(this: *const @This(), start_index: usize, end_index: usize) Data { + return switch (this.*) { .owned => .{ .temporary = this.owned.slice()[start_index..end_index] }, .temporary => .{ .temporary = this.temporary[start_index..end_index] }, .empty => .{ .empty = {} }, + .inline_storage => .{ .temporary = this.inline_storage.slice()[start_index..end_index] }, }; } - pub fn sliceZ(this: @This()) [:0]const u8 { - return switch (this) { + pub fn sliceZ(this: *const @This()) [:0]const u8 { + return switch (this.*) { .owned => this.owned.slice()[0..this.owned.len :0], .temporary => this.temporary[0..this.temporary.len :0], .empty => "", + .inline_storage => this.inline_storage.slice()[0..this.inline_storage.len :0], }; } };