Compare commits

...

3 Commits

Author SHA1 Message Date
Sosuke Suzuki
3950a801ce test(sql): fix MySQL BigInt binding test expectations
Small values in BIGINT columns are returned as Number (not BigInt) by
MySQL even with bigint: true. Focus the test on:
1. INSERTs succeed without ERR_OUT_OF_RANGE (the actual regression)
2. Boundary values (i64 min/max, u64 max) round-trip as BigInt
2026-02-26 19:05:03 +09:00
Sosuke Suzuki
8f109e2675 test(sql): fix MySQL BigInt binding tests to use BIGINT columns
The previous tests used `SELECT ${100n} as x` which returns the value
as a regular number, not a BigInt (MySQL type-infers the result column
independent of the parameter type).

Fixed by using a temporary table with BIGINT/BIGINT UNSIGNED columns
and doing INSERT + SELECT to verify the full round-trip. Also use
toMatchObject({ code: "ERR_OUT_OF_RANGE" }) per review feedback.
2026-02-26 14:56:19 +09:00
Sosuke Suzuki
a118c9e636 fix(sql): fix inverted logic in JSC__isBigIntInInt64Range/UInt64Range
The C++ implementation had two compounding bugs:
1. Parameter names were swapped: Zig passes (min, max) but C++ declared (max, min)
2. The logic used OR short-circuit instead of AND, returning true on the first check

Combined, this caused the function to return true when the BigInt was
OUTSIDE the range, and false when it was INSIDE — the exact opposite of
the intended behavior.

Impact:
- All BigInt parameter binding in Bun.SQL (MySQL) would fail with
  ERR_OUT_OF_RANGE for valid in-range values
- Out-of-range BigInts would be silently accepted and truncated

Fix: Change parameter order to (min, max) and use proper AND logic
(return false if < min, otherwise check <= max).

Added regression tests via bun:internal-for-testing that directly
exercise both functions, plus MySQL integration tests for BigInt
parameter binding.
2026-02-26 12:54:19 +09:00
6 changed files with 182 additions and 18 deletions

View File

@@ -42,4 +42,35 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck, (JSC::JSGlobalObject * glob
return encodedJSUndefined();
}
extern "C" bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t min, int64_t max);
extern "C" bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t min, uint64_t max);
// For testing JSC__isBigIntInInt64Range / JSC__isBigIntInUInt64Range from JS.
// args: (bigint, min: number|bigint, max: number|bigint, unsigned: boolean)
JSC_DEFINE_HOST_FUNCTION(jsFunction_isBigIntInRange, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue value = callFrame->argument(0);
JSValue minArg = callFrame->argument(1);
JSValue maxArg = callFrame->argument(2);
bool isUnsigned = callFrame->argument(3).toBoolean(globalObject);
RETURN_IF_EXCEPTION(scope, {});
if (isUnsigned) {
uint64_t min = minArg.toBigUInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
uint64_t max = maxArg.toBigUInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(JSC__isBigIntInUInt64Range(JSValue::encode(value), min, max)));
}
int64_t min = minArg.toBigInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
int64_t max = maxArg.toBigInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(JSC__isBigIntInInt64Range(JSValue::encode(value), min, max)));
}
}

View File

@@ -8,5 +8,6 @@ namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsFunction_arrayBufferViewHasBuffer);
JSC_DECLARE_HOST_FUNCTION(jsFunction_hasReifiedStatic);
JSC_DECLARE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck);
JSC_DECLARE_HOST_FUNCTION(jsFunction_isBigIntInRange);
}

View File

@@ -5265,34 +5265,32 @@ 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;
auto result = JSBigInt::compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::LessThan)
return false;
result = JSBigInt::compare(bigInt, max);
return result != 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;
auto result = JSBigInt::compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::LessThan)
return false;
result = JSBigInt::compare(bigInt, max);
return result != 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))

View File

@@ -194,6 +194,12 @@ export const getDevServerDeinitCount = $bindgenFn("DevServer.bind.ts", "getDeini
export const getCounters = $newZigFunction("Counters.zig", "createCountersObject", 0);
export const hasNonReifiedStatic = $newCppFunction("InternalForTesting.cpp", "jsFunction_hasReifiedStatic", 1);
export const isBigIntInRange: (value: bigint, min: bigint, max: bigint, unsigned: boolean) => boolean = $newCppFunction(
"InternalForTesting.cpp",
"jsFunction_isBigIntInRange",
4,
);
interface setSocketOptionsFn {
(socket: Bun.Socket, sendBuffer: 1, size: number): void;
(socket: Bun.Socket, recvBuffer: 2, size: number): void;

View File

@@ -0,0 +1,85 @@
import { isBigIntInRange } from "bun:internal-for-testing";
import { describe, expect, test } from "bun:test";
// Regression test for JSC__isBigIntInInt64Range / JSC__isBigIntInUInt64Range.
// Previously the C++ implementation had swapped (min, max) parameter names AND
// used OR-short-circuit logic, causing the function to return true when the
// value was OUTSIDE the range and false when it was INSIDE.
// This affected MySQL BigInt parameter binding (all in-range values were
// rejected with ERR_OUT_OF_RANGE).
const I64_MIN = -9223372036854775808n;
const I64_MAX = 9223372036854775807n;
const U64_MAX = 18446744073709551615n;
describe("isBigIntInInt64Range (signed)", () => {
test("100n is in [i64_min, i64_max]", () => {
expect(isBigIntInRange(100n, I64_MIN, I64_MAX, false)).toBe(true);
});
test("0n is in [i64_min, i64_max]", () => {
expect(isBigIntInRange(0n, I64_MIN, I64_MAX, false)).toBe(true);
});
test("-100n is in [i64_min, i64_max]", () => {
expect(isBigIntInRange(-100n, I64_MIN, I64_MAX, false)).toBe(true);
});
test("i64_max boundary is in range", () => {
expect(isBigIntInRange(I64_MAX, I64_MIN, I64_MAX, false)).toBe(true);
});
test("i64_min boundary is in range", () => {
expect(isBigIntInRange(I64_MIN, I64_MIN, I64_MAX, false)).toBe(true);
});
test("i64_max + 1 is out of range", () => {
expect(isBigIntInRange(I64_MAX + 1n, I64_MIN, I64_MAX, false)).toBe(false);
});
test("i64_min - 1 is out of range", () => {
expect(isBigIntInRange(I64_MIN - 1n, I64_MIN, I64_MAX, false)).toBe(false);
});
test("very large positive bigint is out of range", () => {
expect(isBigIntInRange(2n ** 128n, I64_MIN, I64_MAX, false)).toBe(false);
});
test("very large negative bigint is out of range", () => {
expect(isBigIntInRange(-(2n ** 128n), I64_MIN, I64_MAX, false)).toBe(false);
});
test("narrow range [0, 100]", () => {
expect(isBigIntInRange(50n, 0n, 100n, false)).toBe(true);
expect(isBigIntInRange(0n, 0n, 100n, false)).toBe(true);
expect(isBigIntInRange(100n, 0n, 100n, false)).toBe(true);
expect(isBigIntInRange(-1n, 0n, 100n, false)).toBe(false);
expect(isBigIntInRange(101n, 0n, 100n, false)).toBe(false);
});
});
describe("isBigIntInUInt64Range (unsigned)", () => {
test("100n is in [0, u64_max]", () => {
expect(isBigIntInRange(100n, 0n, U64_MAX, true)).toBe(true);
});
test("0n is in [0, u64_max]", () => {
expect(isBigIntInRange(0n, 0n, U64_MAX, true)).toBe(true);
});
test("u64_max boundary is in range", () => {
expect(isBigIntInRange(U64_MAX, 0n, U64_MAX, true)).toBe(true);
});
test("u64_max + 1 is out of range", () => {
expect(isBigIntInRange(U64_MAX + 1n, 0n, U64_MAX, true)).toBe(false);
});
test("-1n is out of [0, u64_max]", () => {
expect(isBigIntInRange(-1n, 0n, U64_MAX, true)).toBe(false);
});
test("very large bigint is out of range", () => {
expect(isBigIntInRange(2n ** 128n, 0n, U64_MAX, true)).toBe(false);
});
});

View File

@@ -483,9 +483,7 @@ if (isDockerEnabled()) {
test("Binary", async () => {
const random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a binary(1), b varbinary(1), c blob)`;
const values = [
{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) },
];
const values = [{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) }];
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
const results = await sql`select * from ${sql(random_name)}`;
// return buffers
@@ -497,7 +495,7 @@ if (isDockerEnabled()) {
expect(results2[0].a).toEqual(Buffer.from([1]));
expect(results2[0].b).toEqual(Buffer.from([2]));
expect(results2[0].c).toEqual(Buffer.from([3]));
})
});
test("bulk insert nested sql()", async () => {
await using sql = new SQL({ ...getOptions(), max: 1 });
@@ -966,6 +964,51 @@ if (isDockerEnabled()) {
expect((await sql`select 9223372036854777 as x`)[0].x).toBe(9223372036854777n);
});
// Regression: previously any in-range BigInt parameter would throw
// ERR_OUT_OF_RANGE due to inverted logic in JSC__isBigIntInInt64Range.
describe("bigint parameter binding", () => {
test("binds BigInt values into BIGINT columns without ERR_OUT_OF_RANGE", async () => {
await using sql = new SQL({ ...getOptions(), bigint: true });
const table = "t_" + randomUUIDv7("hex").replaceAll("-", "");
await sql`CREATE TEMPORARY TABLE ${sql(table)} (s BIGINT, u BIGINT UNSIGNED)`;
// These should NOT throw ERR_OUT_OF_RANGE (the bug was that they all did)
await sql`INSERT INTO ${sql(table)} (s) VALUES (${100n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${0n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${-1n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${9223372036854775807n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${-9223372036854775808n})`;
await sql`INSERT INTO ${sql(table)} (u) VALUES (${0n})`;
await sql`INSERT INTO ${sql(table)} (u) VALUES (${9223372036854775808n})`;
await sql`INSERT INTO ${sql(table)} (u) VALUES (${18446744073709551615n})`;
// Verify i64 boundary values round-trip correctly as BigInt
const s = (await sql`SELECT s FROM ${sql(table)} WHERE s IS NOT NULL ORDER BY s`).map(r => r.s);
expect(s).toHaveLength(5);
expect(s[0]).toBe(-9223372036854775808n);
expect(s[s.length - 1]).toBe(9223372036854775807n);
// Verify u64 boundary values round-trip correctly
const u = (await sql`SELECT u FROM ${sql(table)} WHERE u IS NOT NULL ORDER BY u`).map(r => r.u);
expect(u).toHaveLength(3);
expect(u[u.length - 1]).toBe(18446744073709551615n);
});
test("throws ERR_OUT_OF_RANGE for BigInt exceeding u64", async () => {
await using sql = new SQL({ ...getOptions() });
await expect(sql`select ${18446744073709551616n} as x`).rejects.toMatchObject({
code: "ERR_OUT_OF_RANGE",
});
});
test("throws ERR_OUT_OF_RANGE for BigInt below i64 min", async () => {
await using sql = new SQL({ ...getOptions() });
await expect(sql`select ${-9223372036854775809n} as x`).rejects.toMatchObject({
code: "ERR_OUT_OF_RANGE",
});
});
});
test("int is returned as Number", async () => {
expect((await sql`select CAST(123 AS SIGNED) as x`)[0].x).toBe(123);
});