Skip to main content

expo-sqlite

Version: 55.0.6 Provides access to a database using SQLite. The database is persisted across restarts of your app. Offers a modern promise-based API with support for transactions, prepared statements, and more.

Installation

npx expo install expo-sqlite

Usage

import * as SQLite from 'expo-sqlite';

// Open database
const db = await SQLite.openDatabaseAsync('mydb.db');

// Create table
await db.execAsync(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  );
`);

// Insert data
await db.runAsync('INSERT INTO users (name, email) VALUES (?, ?)', 
  'John Doe', 'john@example.com');

// Query data
const users = await db.getAllAsync('SELECT * FROM users');
console.log(users);

API Reference

Database Methods

openDatabaseAsync(databaseName, options)
(databaseName: string, options?: SQLiteOpenOptions) => Promise<SQLiteDatabase>
Opens or creates a SQLite databaseParameters:
  • databaseName (string): Database file name
  • options (SQLiteOpenOptions): Optional configuration
const db = await SQLite.openDatabaseAsync('myapp.db');

// With options
const db = await SQLite.openDatabaseAsync('myapp.db', {
  useNewConnection: true,
});
openDatabaseSync(databaseName, options)
(databaseName: string, options?: SQLiteOpenOptions) => SQLiteDatabase
Synchronous version of openDatabaseAsync
const db = SQLite.openDatabaseSync('myapp.db');
deleteDatabaseAsync(databaseName)
(databaseName: string) => Promise<void>
Deletes a database file
await SQLite.deleteDatabaseAsync('myapp.db');
deleteDatabaseSync(databaseName)
(databaseName: string) => void
Synchronous version of deleteDatabaseAsync

SQLiteDatabase

Query Methods

db.getAllAsync(query, ...params)
(query: string, ...params: any[]) => Promise<any[]>
Executes a query and returns all rows
const users = await db.getAllAsync('SELECT * FROM users WHERE age > ?', 18);
db.getFirstAsync(query, ...params)
(query: string, ...params: any[]) => Promise<any | null>
Executes a query and returns the first row
const user = await db.getFirstAsync('SELECT * FROM users WHERE id = ?', 1);
db.runAsync(query, ...params)
(query: string, ...params: any[]) => Promise<SQLiteRunResult>
Executes a query without returning rowsReturns object with lastInsertRowId and changes
const result = await db.runAsync(
  'INSERT INTO users (name, email) VALUES (?, ?)',
  'Jane Doe',
  'jane@example.com'
);
console.log('Inserted ID:', result.lastInsertRowId);
console.log('Rows changed:', result.changes);
db.execAsync(source)
(source: string) => Promise<void>
Executes multiple SQL statements
await db.execAsync(`
  DROP TABLE IF EXISTS users;
  CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
  INSERT INTO users (name) VALUES ('John');
`);

Prepared Statements

db.prepareAsync(query)
(query: string) => Promise<SQLiteStatement>
Creates a prepared statement for efficient repeated execution
const stmt = await db.prepareAsync('INSERT INTO users (name) VALUES (?)');

for (const name of names) {
  await stmt.executeAsync(name);
}

await stmt.finalizeAsync();

Transactions

db.withTransactionAsync(task)
(task: () => Promise<void>) => Promise<void>
Executes operations within a transactionAutomatically commits on success or rolls back on error
await db.withTransactionAsync(async () => {
  await db.runAsync('INSERT INTO users (name) VALUES (?)', 'Alice');
  await db.runAsync('INSERT INTO users (name) VALUES (?)', 'Bob');
});
db.withExclusiveTransactionAsync(task)
(task: () => Promise<void>) => Promise<void>
Executes operations within an exclusive transactionGuarantees no other queries interrupt the transaction
await db.withExclusiveTransactionAsync(async () => {
  const count = await db.getFirstAsync('SELECT COUNT(*) as count FROM users');
  await db.runAsync('INSERT INTO users (name) VALUES (?)', 'New User');
});

Other Methods

db.closeAsync()
() => Promise<void>
Closes the database connection
await db.closeAsync();
db.isInTransactionAsync()
() => Promise<boolean>
Checks if database is currently in a transaction
db.serializeAsync(databaseName)
(databaseName?: string) => Promise<Uint8Array>
Serializes database to Uint8Array

SQLiteStatement

stmt.executeAsync(...params)
(...params: any[]) => Promise<SQLiteExecuteAsyncResult>
Executes the prepared statement
const result = await stmt.executeAsync('John', 'john@example.com');
stmt.finalizeAsync()
() => Promise<void>
Finalizes and releases the statement
await stmt.finalizeAsync();

Hooks

useSQLiteContext()
() => SQLiteDatabase
Access database from SQLiteProvider context
import { useSQLiteContext } from 'expo-sqlite';

function MyComponent() {
  const db = useSQLiteContext();
  // Use db
}

Types

SQLiteRunResult

lastInsertRowId
number
ID of the last inserted row
changes
number
Number of rows changed

Examples

Basic CRUD Operations

import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('app.db');

// Create
await db.execAsync(`
  CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL,
    completed INTEGER DEFAULT 0
  );
`);

// Insert
const result = await db.runAsync(
  'INSERT INTO todos (text) VALUES (?)',
  'Learn SQLite'
);
console.log('Inserted todo with ID:', result.lastInsertRowId);

// Read all
const todos = await db.getAllAsync('SELECT * FROM todos');

// Read one
const todo = await db.getFirstAsync('SELECT * FROM todos WHERE id = ?', 1);

// Update
await db.runAsync('UPDATE todos SET completed = 1 WHERE id = ?', 1);

// Delete
await db.runAsync('DELETE FROM todos WHERE id = ?', 1);

Using Prepared Statements

import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('app.db');

// Prepare statement for batch inserts
const stmt = await db.prepareAsync(
  'INSERT INTO users (name, email, age) VALUES (?, ?, ?)'
);

const users = [
  ['Alice', 'alice@example.com', 25],
  ['Bob', 'bob@example.com', 30],
  ['Charlie', 'charlie@example.com', 35],
];

try {
  for (const [name, email, age] of users) {
    await stmt.executeAsync(name, email, age);
  }
} finally {
  await stmt.finalizeAsync();
}

console.log('Batch insert complete');

Transactions

import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('banking.db');

async function transferFunds(fromId: number, toId: number, amount: number) {
  await db.withTransactionAsync(async () => {
    // Deduct from sender
    await db.runAsync(
      'UPDATE accounts SET balance = balance - ? WHERE id = ?',
      amount,
      fromId
    );
    
    // Add to receiver
    await db.runAsync(
      'UPDATE accounts SET balance = balance + ? WHERE id = ?',
      amount,
      toId
    );
  });
  
  console.log('Transfer completed');
}

Complex Queries

import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('app.db');

// Join query
const results = await db.getAllAsync(`
  SELECT 
    users.name,
    orders.total,
    orders.created_at
  FROM users
  JOIN orders ON users.id = orders.user_id
  WHERE orders.total > ?
  ORDER BY orders.created_at DESC
  LIMIT ?
`, 100, 10);

// Aggregation
const stats = await db.getFirstAsync(`
  SELECT 
    COUNT(*) as total_orders,
    SUM(total) as revenue,
    AVG(total) as avg_order
  FROM orders
  WHERE created_at > ?
`, Date.now() - 86400000); // Last 24 hours

console.log('Stats:', stats);

React Hook Usage

import { SQLiteProvider, useSQLiteContext } from 'expo-sqlite';
import { useEffect, useState } from 'react';
import { View, Text, FlatList } from 'react-native';

export default function App() {
  return (
    <SQLiteProvider databaseName="app.db" onInit={initDB}>
      <TodoList />
    </SQLiteProvider>
  );
}

async function initDB(db: SQLite.SQLiteDatabase) {
  await db.execAsync(`
    CREATE TABLE IF NOT EXISTS todos (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      text TEXT NOT NULL,
      completed INTEGER DEFAULT 0
    );
  `);
}

function TodoList() {
  const db = useSQLiteContext();
  const [todos, setTodos] = useState<any[]>([]);

  useEffect(() => {
    loadTodos();
  }, []);

  async function loadTodos() {
    const result = await db.getAllAsync('SELECT * FROM todos');
    setTodos(result);
  }

  async function addTodo(text: string) {
    await db.runAsync('INSERT INTO todos (text) VALUES (?)', text);
    await loadTodos();
  }

  return (
    <View>
      <FlatList
        data={todos}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => <Text>{item.text}</Text>}
      />
    </View>
  );
}

Migration Example

import * as SQLite from 'expo-sqlite';

const CURRENT_VERSION = 2;

async function migrateDatabase() {
  const db = await SQLite.openDatabaseAsync('app.db');
  
  let version = 0;
  try {
    const result = await db.getFirstAsync<{ user_version: number }>(
      'PRAGMA user_version'
    );
    version = result?.user_version ?? 0;
  } catch (e) {
    // First time
  }

  if (version === 0) {
    // Initial schema
    await db.execAsync(`
      CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
      PRAGMA user_version = 1;
    `);
    version = 1;
  }

  if (version === 1) {
    // Migration to v2
    await db.execAsync(`
      ALTER TABLE users ADD COLUMN email TEXT;
      PRAGMA user_version = 2;
    `);
    version = 2;
  }

  console.log('Database migrated to version', version);
  return db;
}

Platform Support

PlatformSupported
iOS
Android
Web✅ (WebSQL/IndexedDB)

Best Practices

  1. Use Prepared Statements: For repeated queries, use prepareAsync() for better performance
  2. Transactions: Use transactions for multiple related operations
  3. Close Connections: Always close database connections when done
  4. Parameterized Queries: Use ? placeholders to prevent SQL injection
  5. Error Handling: Wrap database operations in try/catch blocks
  6. Migrations: Implement version-based migrations for schema changes
Always use parameterized queries with ? placeholders instead of string concatenation to prevent SQL injection attacks.

Resources