Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5516,34 +5516,28 @@ extern "C" void JSC__JSValue__forEachPropertyNonIndexed(JSC::EncodedJSValue JSVa
JSC__JSValue__forEachPropertyImpl<true>(JSValue0, globalObject, arg2, iter);
}

extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t max, uint64_t min)
extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t min, uint64_t max)
{
JSValue jsValue = JSValue::decode(value);
if (!jsValue.isHeapBigInt())
return false;

JSC::JSBigInt* bigInt = jsValue.asHeapBigInt();
auto result = bigInt->compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) {
return true;
}
result = bigInt->compare(bigInt, max);
return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal;
if (JSBigInt::compare(bigInt, min) == JSBigInt::ComparisonResult::LessThan)
return false;
return JSBigInt::compare(bigInt, max) != JSBigInt::ComparisonResult::GreaterThan;
}

extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t max, int64_t min)
extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t min, int64_t max)
{
JSValue jsValue = JSValue::decode(value);
if (!jsValue.isHeapBigInt())
return false;

JSC::JSBigInt* bigInt = jsValue.asHeapBigInt();
auto result = bigInt->compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) {
return true;
}
result = bigInt->compare(bigInt, max);
return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal;
if (JSBigInt::compare(bigInt, min) == JSBigInt::ComparisonResult::LessThan)
return false;
return JSBigInt::compare(bigInt, max) != JSBigInt::ComparisonResult::GreaterThan;
}

[[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol))
Expand Down
243 changes: 243 additions & 0 deletions test/js/sql/sql-mysql-bigint-param.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// https://github.com/oven-sh/bun/pull/32265
// Uses a minimal mock MySQL server so the test runs without Docker.

import { SQL } from "bun";
import { describe, expect, test } from "bun:test";
import { once } from "events";
import net from "net";

// --- MySQL wire helpers ----------------------------------------------------

function u16le(n: number): Buffer {
return Buffer.from([n & 0xff, (n >> 8) & 0xff]);
}
function u24le(n: number): Buffer {
return Buffer.from([n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff]);
}
function u32le(n: number): Buffer {
return Buffer.from([n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff]);
}
function packet(seq: number, payload: Buffer): Buffer {
return Buffer.concat([u24le(payload.length), Buffer.from([seq]), payload]);
}
function lenenc(n: number): Buffer {
if (n < 0xfb) return Buffer.from([n]);
if (n < 0xffff) return Buffer.concat([Buffer.from([0xfc]), u16le(n)]);
throw new Error("lenenc: not needed for this test");
}
function lenencStr(s: string): Buffer {
const buf = Buffer.from(s, "utf-8");
return Buffer.concat([lenenc(buf.length), buf]);
}

const CLIENT_PROTOCOL_41 = 1 << 9;
const CLIENT_SECURE_CONNECTION = 1 << 15;
const CLIENT_PLUGIN_AUTH = 1 << 19;
const CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21;
const CLIENT_DEPRECATE_EOF = 1 << 24;
const SERVER_CAPS =
CLIENT_PROTOCOL_41 |
CLIENT_SECURE_CONNECTION |
CLIENT_PLUGIN_AUTH |
CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA |
CLIENT_DEPRECATE_EOF;

const MYSQL_TYPE_LONGLONG = 0x08;

function handshakeV10(): Buffer {
const authData1 = Buffer.alloc(8, 0x61);
const authData2 = Buffer.alloc(13, 0x62);
authData2[12] = 0;
return packet(
0,
Buffer.concat([
Buffer.from([10]),
Buffer.from("mock-5.7.0\0"),
u32le(1),
authData1,
Buffer.from([0]),
u16le(SERVER_CAPS & 0xffff),
Buffer.from([0x2d]),
u16le(0x0002),
u16le((SERVER_CAPS >>> 16) & 0xffff),
Buffer.from([21]),
Buffer.alloc(10, 0),
authData2,
Buffer.from("mysql_native_password\0"),
]),
);
}

function okPacket(seq: number, header = 0x00): Buffer {
return packet(seq, Buffer.from([header, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]));
}

function columnDef(name: string, type: number, flags = 0): Buffer {
return Buffer.concat([
lenencStr("def"),
lenencStr(""),
lenencStr("t"),
lenencStr("t"),
lenencStr(name),
lenencStr(name),
Buffer.from([0x0c]),
u16le(33),
u32le(1024),
Buffer.from([type]),
u16le(flags),
Buffer.from([0]),
Buffer.from([0, 0]),
]);
}

type Captured = { type: number; unsigned: boolean; raw: Buffer };

function startMockServer(onExecute: (c: Captured) => void) {
const server = net.createServer(socket => {
let buffered = Buffer.alloc(0);
let authed = false;
let stmtId = 0;
socket.write(handshakeV10());
socket.on("data", chunk => {
buffered = Buffer.concat([buffered, chunk]);
while (buffered.length >= 4) {
const len = buffered[0] | (buffered[1] << 8) | (buffered[2] << 16);
if (buffered.length < 4 + len) break;
const seq = buffered[3];
const payload = buffered.subarray(4, 4 + len);
buffered = buffered.subarray(4 + len);
if (!authed) {
authed = true;
socket.write(okPacket(seq + 1));
continue;
}
const cmd = payload[0];
if (cmd === 0x16 /* COM_STMT_PREPARE */) {
// 1 param, 1 column ("x" BIGINT).
let s = seq + 1;
const packets: Buffer[] = [];
packets.push(
packet(
s++,
Buffer.concat([
Buffer.from([0x00]),
u32le(++stmtId),
u16le(1), // num_columns
u16le(1), // num_params
Buffer.from([0x00]),
u16le(0),
]),
),
);
packets.push(packet(s++, columnDef("?", MYSQL_TYPE_LONGLONG)));
packets.push(packet(s++, columnDef("x", MYSQL_TYPE_LONGLONG)));
socket.write(Buffer.concat(packets));
} else if (cmd === 0x17 /* COM_STMT_EXECUTE */) {
// Decode the single bound parameter. Layout for 1 param:
// cmd(1) stmt_id(4) flags(1) iter(4) null_bitmap(1)
// new_params_bind(1) [type(1) flags(1)] value(8)
let o = 1 + 4 + 1 + 4;
const nullBitmap = payload[o++];
const newBind = payload[o++];
let type = MYSQL_TYPE_LONGLONG;
let unsigned = false;
if (newBind) {
type = payload[o++];
unsigned = (payload[o++] & 0x80) !== 0;
}
const raw = Buffer.from(payload.subarray(o, o + 8));
if (nullBitmap === 0) onExecute({ type, unsigned, raw });

// Reply with a 1-column BIGINT result echoing the bound value.
let s = seq + 1;
const packets: Buffer[] = [];
packets.push(packet(s++, Buffer.from([1])));
packets.push(packet(s++, columnDef("x", MYSQL_TYPE_LONGLONG, unsigned ? 1 << 5 : 0)));
packets.push(packet(s++, Buffer.concat([Buffer.from([0x00]), Buffer.from([0x00]), raw])));
packets.push(packet(s++, Buffer.from([0xfe, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00])));
socket.write(Buffer.concat(packets));
} else if (cmd === 0x03 /* COM_QUERY */) {
socket.write(okPacket(seq + 1));
} else if (cmd === 0x19 /* COM_STMT_CLOSE */ || cmd === 0x1a /* COM_STMT_RESET */) {
// no response expected
} else {
socket.end();
}
}
});
});
server.listen(0, "127.0.0.1");
return server;
}

const I64_MIN = -9223372036854775808n;
const I64_MAX = 9223372036854775807n;
const U64_MAX = 18446744073709551615n;

describe("MySQL BigInt parameter binding", () => {
test("in-range BigInt params are accepted and round-trip on the wire", async () => {
const captured: Captured[] = [];
const server = startMockServer(c => captured.push(c));
await once(server, "listening");
const { port } = server.address() as net.AddressInfo;
try {
await using sql = new SQL({ url: `mysql://root@127.0.0.1:${port}/db`, max: 1 });

const cases: Array<{ in: bigint; signed: boolean }> = [
{ in: 5n, signed: true },
{ in: 0n, signed: true },
{ in: -1n, signed: true },
{ in: I64_MAX, signed: true },
{ in: I64_MIN, signed: true },
// Above i64::MAX but within u64 range: bound as unsigned LONGLONG.
{ in: I64_MAX + 1n, signed: false },
{ in: U64_MAX, signed: false },
];

for (const c of cases) {
captured.length = 0;
await sql.unsafe("SELECT ? AS x", [c.in]);
expect(captured.length).toBe(1);
const got = captured[0];
const wire = got.unsigned ? got.raw.readBigUInt64LE(0) : got.raw.readBigInt64LE(0);
expect({ in: c.in, type: got.type, unsigned: got.unsigned, wire }).toEqual({
in: c.in,
type: MYSQL_TYPE_LONGLONG,
unsigned: !c.signed,
wire: c.in,
});
}
} finally {
await new Promise<void>(r => server.close(() => r()));
}
});

test("out-of-range BigInt params reject with ERR_OUT_OF_RANGE", async () => {
const captured: Captured[] = [];
const server = startMockServer(c => captured.push(c));
await once(server, "listening");
const { port } = server.address() as net.AddressInfo;
try {
await using sql = new SQL({ url: `mysql://root@127.0.0.1:${port}/db`, max: 1 });

for (const v of [U64_MAX + 1n, I64_MIN - 1n, 2n ** 100n, -(2n ** 100n)]) {
captured.length = 0;
const result = await sql.unsafe("SELECT ? AS x", [v]).then(
rows => ({ ok: true as const, rows }),
err => ({ ok: false as const, code: (err as any)?.code, message: String((err as any)?.message ?? err) }),
);
expect({ v, result, executed: captured.length }).toEqual({
v,
result: {
ok: false,
code: "ERR_OUT_OF_RANGE",
message: expect.stringContaining("out of range"),
},
executed: 0,
});
}
} finally {
await new Promise<void>(r => server.close(() => r()));
}
});
});
Loading