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)");