Core features

Integer fidelity

See which values return as plain numbers, which return as exact BigInt, and how to keep 64-bit precision on every runtime.

Table of Contents

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); anything past that boundary gets rounded. 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, then looks at how each driver behaves and what to do on Expo, where BigInt does not exist.

Store 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: a plain number literal with the same digits would be rounded to 9007199254740992 before it ever reaches the driver. Start integers.ts here.

integers.ts
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.

integers.ts
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 number

The tracking code comes back as a bigint with every digit intact, and the parcel count is an ordinary number. The boundary is inclusive: 9007199254740991 itself, or its negative, is still a plain number, and the first value past either end becomes bigint.

How each driver behaves

DriverBehaviour
@delali/sirannon-db/driver/better-sqlite3The driver turns on better-sqlite3's defaultSafeIntegers so that integers arrive as bigint, then narrows safe-range values back to number.
@delali/sirannon-db/driver/nodeThe driver enables setReadBigInts on every statement of the built-in node:sqlite module and applies the same narrowing.
@delali/sirannon-db/driver/bunThe driver opens bun:sqlite with safeIntegers: true and applies the same narrowing.
@delali/sirannon-db/driver/wa-sqlitewa-sqlite conforms on its own: every value it returns as a number is exact, and a value a double cannot hold arrives as bigint, so the driver passes results straight through.
@delali/sirannon-db/driver/expoexpo-sqlite has no BigInt support, so a value past the safe range loses precision and returns as the nearest double.

None of this breaks down over the network either. 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. Store 9007199254740993 on Expo and you read back 9007199254740992, the closest value a double can hold. Plain JavaScript shows the same rounding.

const stored = 9007199254740993n
console.log(Number(stored))
9007199254740992

The 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 survives storage, queries, and replication byte for byte. You give up numeric ordering and SQL arithmetic on that column, which rarely matters for identifiers.