SQLite stores integers as 64-bit signed values. A JavaScript number can only represent integers exactly between -9007199254740991 and 9007199254740991 (2^53 - 1, Number.MAX_SAFE_INTEGER); the engine rounds anything past that boundary. Sirannon bridges the gap the same way on every runtime with BigInt support: integers inside the safe range come back as plain number values, and anything larger comes back as an exact bigint. This page builds one file, integers.ts, that stores and reads a value past the safe boundary, and it then sets out how each driver behaves and what to do on Expo, where BigInt does not exist.
Sirannon writes the database file into a directory you create yourself, so make one before you run any of the code below.
mkdir -p dataStore a value past the safe range
Insert a shipment whose tracking code is 9007199254740993, the smallest positive integer a double cannot hold. Write the literal with the n suffix, because a plain number literal with the same digits would round to 9007199254740992 before it ever reached the driver. Start integers.ts here.
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('logistics', './data/logistics.db')
await db.execute(
'CREATE TABLE IF NOT EXISTS shipments (id INTEGER PRIMARY KEY, tracking_code INTEGER NOT NULL, parcels INTEGER NOT NULL)'
)
await db.execute('INSERT INTO shipments (tracking_code, parcels) VALUES (?, ?)', [
9007199254740993n,
12,
])Read the exact value back
Query the row and check the types of both columns.
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('logistics', './data/logistics.db')
await db.execute(
'CREATE TABLE IF NOT EXISTS shipments (id INTEGER PRIMARY KEY, tracking_code INTEGER NOT NULL, parcels INTEGER NOT NULL)'
)
await db.execute('INSERT INTO shipments (tracking_code, parcels) VALUES (?, ?)', [
9007199254740993n,
12,
])
const rows = await db.query<{ tracking_code: bigint; parcels: number }>(
'SELECT tracking_code, parcels FROM shipments'
)
console.log(rows[0])
console.log(typeof rows[0].tracking_code, typeof rows[0].parcels){ tracking_code: 9007199254740993n, parcels: 12 }
bigint numberThe tracking code comes back as a bigint with every digit intact, and the parcel count is an ordinary number. The boundary is inclusive, so 9007199254740991 itself, and its negative, stay plain numbers, while the first value past either end becomes a bigint.
How each driver behaves
| Driver | Behaviour |
|---|---|
@delali/sirannon-db/driver/better-sqlite3 | The driver turns on better-sqlite3's defaultSafeIntegers so that it returns integers as bigint, then narrows safe-range values back to number. |
@delali/sirannon-db/driver/node | The driver enables setReadBigInts on every statement of the built-in node:sqlite module and applies the same narrowing. |
@delali/sirannon-db/driver/bun | The driver opens bun:sqlite with safeIntegers: true and applies the same narrowing. |
@delali/sirannon-db/driver/wa-sqlite | wa-sqlite conforms on its own, because every value it returns as a number is exact and it returns a value a double cannot hold as a bigint, so the driver passes each result straight through. |
@delali/sirannon-db/driver/expo | expo-sqlite has no BigInt support, so a value past the safe range loses precision and returns as the nearest double. |
The same guarantee holds over the network. A server backed by any of the first four drivers wraps large integers in a tagged JSON envelope, covered in value encoding over JSON, and the client SDK unwraps them back into bigint on the other side.
Plan for Expo
expo-sqlite has no BigInt support at all. Its bind values are strings, numbers, booleans, null, and Uint8Array, so every integer it reads comes back as a double. Stored on Expo, 9007199254740993 reads back as 9007199254740992, the closest value a double can hold. Plain JavaScript shows the same rounding.
const stored = 9007199254740993n
console.log(Number(stored))9007199254740992The practical fix is to store the big value as text. Keep numeric columns inside the safe range, and store anything larger, such as an order reference from a payment provider, as TEXT and compare it as a string. The text passes through storage, queries, and replication byte for byte. You give up numeric ordering and SQL arithmetic on that column, which seldom matters for an identifier you only compare for equality.