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
18 changes: 18 additions & 0 deletions docs/runtime/sqlite.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<Note>
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;
Comment thread
robobun marked this conversation as resolved.
```

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.

</Note>

### `safeIntegers: true`

When `safeIntegers` is `true`, `bun:sqlite` returns integers as `bigint` types:
Expand Down
16 changes: 9 additions & 7 deletions src/jsc/bindings/sqlite/JSSQLStatement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "JSSQLStatement.h"
#include "ScriptExecutionContext.h"
#include <JavaScriptCore/JSObjectInlines.h>
#include <JavaScriptCore/MathCommon.h>
#include <limits>
#include <wtf/text/ExternalStringImpl.h>

Expand Down Expand Up @@ -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<int64_t>(number)));
Comment thread
robobun marked this conversation as resolved.
} 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);
Expand Down
51 changes: 51 additions & 0 deletions test/js/bun/sqlite/sqlite.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
Expand Down
Loading