diff --git a/docs/runtime/sqlite.mdx b/docs/runtime/sqlite.mdx
index bed3c16f5917..1e13dbae48fd 100644
--- a/docs/runtime/sqlite.mdx
+++ b/docs/runtime/sqlite.mdx
@@ -491,6 +491,24 @@ SQLite supports signed 64-bit integers, but JavaScript only supports signed 53-b
`bigint` input is supported everywhere, but by default `bun:sqlite` returns integers as `number` types. If you need to handle integers larger than 2^53, set the `safeIntegers` option to `true` when creating a `Database` instance. The option also validates that `bigint` values passed to `bun:sqlite` do not exceed 64 bits.
+`bun:sqlite` binds a `number` parameter as `INTEGER` when the value is a safe integer (`Number.isSafeInteger()`). It binds every other `number`, and `-0`, as a floating-point `REAL`. SQLite stores `NaN` as `NULL`.
+
+
+Bun v1.4.2 and earlier bound a safe integer as `REAL` when it was 2^51 or more, or less than -2^51. A `TEXT` column stored that value with a `.0` suffix, for example `'4503599627370497.0'`. A query that binds the same `number` compares it as `'4503599627370497'` and does not match that row. To convert the rows that an earlier version wrote, run a statement like this one for each affected table and column:
+
+```sql
+UPDATE users SET id = CAST(CAST(id AS REAL) AS INTEGER)
+WHERE id LIKE '%.0'
+ AND rtrim(substr(id, 1, length(id) - 2), '0123456789') IN ('', '-')
+ AND abs(CAST(id AS REAL)) BETWEEN 2251799813685248 AND 9007199254740991;
+```
+
+Run the statement before the upgraded application writes to the table. If a `PRIMARY KEY` or `UNIQUE` column already holds both forms of one value, SQLite rejects the statement and changes no rows. Merge or delete one row of each pair first.
+
+SQLite 3.51 and earlier stored 15 significant digits in that case, for example `'4.5035996273705e+15'`. Those rows do not hold the exact value.
+
+
+
### `safeIntegers: true`
When `safeIntegers` is `true`, `bun:sqlite` returns integers as `bigint` types:
diff --git a/src/jsc/bindings/sqlite/JSSQLStatement.cpp b/src/jsc/bindings/sqlite/JSSQLStatement.cpp
index 7c3c011e2ffd..d4f2cbc552fe 100644
--- a/src/jsc/bindings/sqlite/JSSQLStatement.cpp
+++ b/src/jsc/bindings/sqlite/JSSQLStatement.cpp
@@ -15,6 +15,7 @@
#include "JSSQLStatement.h"
#include "ScriptExecutionContext.h"
#include
+#include
#include
#include
@@ -913,15 +914,16 @@ static inline bool rebindValue(JSC::JSGlobalObject* lexicalGlobalObject, sqlite3
CHECK_BIND(sqlite3_bind_null(stmt, i));
} else if (value.isBoolean()) {
CHECK_BIND(sqlite3_bind_int(stmt, i, value.toBoolean(lexicalGlobalObject) ? 1 : 0));
- } else if (value.isAnyInt()) {
- int64_t val = value.asAnyInt();
- if (val < INT_MIN || val > INT_MAX) {
- CHECK_BIND(sqlite3_bind_int64(stmt, i, val));
+ } else if (value.isInt32()) {
+ CHECK_BIND(sqlite3_bind_int(stmt, i, value.asInt32()));
+ } else if (value.isNumber()) {
+ double number = value.asDouble();
+ // Not isAnyInt(): that is JSC's Int52 test, which rejects 2^51 and up. -0 binds as REAL to keep its sign.
+ if (JSC::isSafeInteger(number) && !JSC::isNegativeZero(number)) {
+ CHECK_BIND(sqlite3_bind_int64(stmt, i, static_cast(number)));
} else {
- CHECK_BIND(sqlite3_bind_int(stmt, i, val))
+ CHECK_BIND(sqlite3_bind_double(stmt, i, number));
}
- } else if (value.isNumber()) {
- CHECK_BIND(sqlite3_bind_double(stmt, i, value.asDouble()))
} else if (value.isString()) {
auto* str = value.toStringOrNull(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, false);
diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js
index 81297e59b5fd..ea2a28217d58 100644
--- a/test/js/bun/sqlite/sqlite.test.js
+++ b/test/js/bun/sqlite/sqlite.test.js
@@ -522,6 +522,57 @@ it("int52", () => {
expect(db.query("SELECT * FROM test").get().int64).toBe(Number.MAX_SAFE_INTEGER);
});
+it("binds every safe integer number as INTEGER", () => {
+ const db = new Database(":memory:");
+ const bound = db.query("SELECT typeof(?1) AS type, CAST(?1 AS TEXT) AS text");
+
+ // A Float64Array element is a double in JSC, also when its value fits in an int32.
+ const sevenAsDouble = new Float64Array([7])[0];
+ const safeIntegers = [
+ 0,
+ sevenAsDouble,
+ 2 ** 31,
+ -(2 ** 31) - 1,
+ 2 ** 51 - 1,
+ 2 ** 51,
+ 2 ** 52 + 1,
+ Number.MAX_SAFE_INTEGER,
+ -(2 ** 51),
+ -(2 ** 51) - 1,
+ Number.MIN_SAFE_INTEGER,
+ ];
+ expect(safeIntegers.map(value => bound.get(value))).toEqual([
+ { type: "integer", text: "0" },
+ { type: "integer", text: "7" },
+ { type: "integer", text: "2147483648" },
+ { type: "integer", text: "-2147483649" },
+ { type: "integer", text: "2251799813685247" },
+ { type: "integer", text: "2251799813685248" },
+ { type: "integer", text: "4503599627370497" },
+ { type: "integer", text: "9007199254740991" },
+ { type: "integer", text: "-2251799813685248" },
+ { type: "integer", text: "-2251799813685249" },
+ { type: "integer", text: "-9007199254740991" },
+ ]);
+
+ // These are not safe integers. They keep the REAL type.
+ const reals = [2 ** 53, -(2 ** 53), 2 ** 63, 1e300, 1.5, -0, Infinity];
+ expect(reals.map(value => bound.get(value).type)).toEqual(["real", "real", "real", "real", "real", "real", "real"]);
+ expect(Object.is(db.query("SELECT ? AS value").get(-0).value, -0)).toBe(true);
+ // sqlite3_bind_double() turns NaN into NULL.
+ expect(bound.get(NaN)).toEqual({ type: "null", text: null });
+
+ // A column with no type affinity stores the type of the bound value.
+ db.run("CREATE TABLE events (id)");
+ const insert = db.query("INSERT INTO events VALUES (?)");
+ for (const id of [2 ** 51 - 1, 2 ** 51, Number.MAX_SAFE_INTEGER]) insert.run(id);
+ expect(db.query("SELECT id, typeof(id) AS type, CAST(id AS TEXT) AS text FROM events").all()).toEqual([
+ { id: 2251799813685247, type: "integer", text: "2251799813685247" },
+ { id: 2251799813685248, type: "integer", text: "2251799813685248" },
+ { id: 9007199254740991, type: "integer", text: "9007199254740991" },
+ ]);
+});
+
it("typechecks", () => {
const db = Database.open(":memory:");
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");