Skip to main content

SQLite Adapter

The SQLite adapter provides lightweight embedded database support for Text2SQL. Perfect for local development, testing, and single-file databases.

Installation

npm install @deepagents/text2sql
npm install better-sqlite3
npm install -D @types/better-sqlite3

Basic Usage

import { Sqlite, tables, views, info } from '@deepagents/text2sql/sqlite';
import Database from 'better-sqlite3';

const db = new Database('./my-database.db');

const adapter = new Sqlite({
  execute: (sql) => db.prepare(sql).all(),
  grounding: [
    tables(),
    views(),
    info()
  ]
});

Configuration Options

interface SqliteAdapterOptions {
  // Required: Function to execute SQL queries
  execute: (sql: string) => any[] | Promise<any[]>;
  
  // Optional: Custom validation function
  validate?: (sql: string) => string | void | Promise<string | void>;
  
  // Required: Grounding functions for schema introspection
  grounding: GroundingFn[];
}

Execute Function

The execute function receives SQL and returns query results.

Using better-sqlite3

import Database from 'better-sqlite3';

const db = new Database('./database.db');

const adapter = new Sqlite({
  execute: (sql) => db.prepare(sql).all(),
  grounding: [tables()]
});

Using node-sqlite3

import sqlite3 from 'sqlite3';
import { open } from 'sqlite';

const db = await open({
  filename: './database.db',
  driver: sqlite3.Database
});

const adapter = new Sqlite({
  execute: async (sql) => {
    return await db.all(sql);
  },
  grounding: [tables()]
});

Custom Validation

By default, validation uses EXPLAIN to check SQL syntax. You can provide a custom validator:
const adapter = new Sqlite({
  execute: (sql) => db.prepare(sql).all(),
  validate: async (sql) => {
    // Custom validation logic
    if (sql.includes('DROP')) {
      throw new Error('DROP statements not allowed');
    }
    
    // Use EXPLAIN for syntax check
    try {
      db.prepare(`EXPLAIN ${sql}`).all();
    } catch (error) {
      throw error;
    }
  },
  grounding: [tables()]
});

Grounding Functions

All SQLite grounding functions are available:
import {
  tables,        // Tables, columns, and primary keys
  views,         // Database views
  info,          // SQLite version and metadata
  indexes,       // Index information
  constraints,   // UNIQUE, CHECK, FOREIGN KEY constraints
  rowCount,      // Table row counts and size hints
  columnStats,   // Min/max/null distribution
  columnValues   // Distinct values for low-cardinality columns
} from '@deepagents/text2sql/sqlite';

const adapter = new Sqlite({
  execute: (sql) => db.prepare(sql).all(),
  grounding: [
    tables(),
    views(),
    info(),
    indexes(),
    constraints(),
    rowCount(),
    columnStats(),
    columnValues({ limit: 20 })
  ]
});

SQLite-Specific Features

No Schema Support

SQLite does not have separate schemas. All tables are in a single namespace:
// No schema prefix needed
const sql = 'SELECT * FROM customers';

Identifier Quoting

SQLite uses double quotes for identifiers:
adapter.quoteIdentifier('my_table');  // "my_table"
adapter.quoteIdentifier('my column'); // "my column"

String Escaping

SQLite escapes single quotes by doubling them:
adapter.escape("O'Reilly");  // O''Reilly

LIMIT Clause

SQLite uses LIMIT for row limits:
SELECT * FROM customers LIMIT 10

Error Handling

SQLite errors are automatically classified:
try {
  const sql = await text2sql.toSql('Show me invalid_table');
} catch (error) {
  console.error(error);
}
Error types:
Error PatternTypeSuggestion
no such table: XMISSING_TABLECheck the database schema for the correct table name
no such column: XINVALID_COLUMNCheck the table schema for correct column names
ambiguous column name: XINVALID_COLUMNUse table aliases to disambiguate
near "X": syntax errorSYNTAX_ERRORReview query structure, keywords, and punctuation
no tables specifiedSYNTAX_ERRORAdd FROM clause
attempt to write a readonly databaseCONSTRAINT_ERRORDatabase is read-only

Complete Example

sqlite-example.ts
import { Text2Sql } from '@deepagents/text2sql';
import { Sqlite, tables, views, info, indexes } from '@deepagents/text2sql/sqlite';
import { InMemoryContextStore } from '@deepagents/context';
import { openai } from '@ai-sdk/openai';
import Database from 'better-sqlite3';

// Create database
const db = new Database('./ecommerce.db');

// Create sample schema
db.exec(`
  CREATE TABLE IF NOT EXISTS customers (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  );
  
  CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    total REAL NOT NULL,
    status TEXT NOT NULL,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
  );
  
  CREATE INDEX IF NOT EXISTS idx_orders_customer
    ON orders(customer_id);
  
  CREATE INDEX IF NOT EXISTS idx_orders_status
    ON orders(status);
`);

// Create adapter
const adapter = new Sqlite({
  execute: (sql) => {
    console.log('Executing:', sql);
    return db.prepare(sql).all();
  },
  grounding: [
    tables(),
    views(),
    info(),
    indexes()
  ]
});

// Create Text2SQL instance
const text2sql = new Text2Sql({
  version: 'v1',
  model: openai('gpt-4o'),
  adapter,
  store: new InMemoryContextStore()
});

// Generate queries
async function demo() {
  // Simple query
  const sql1 = await text2sql.toSql('Show all customers');
  console.log('Query 1:', sql1);
  console.log('Results:', adapter.execute(sql1));
  
  // Join query
  const sql2 = await text2sql.toSql('Show customer names with their order count');
  console.log('Query 2:', sql2);
  console.log('Results:', adapter.execute(sql2));
  
  // Aggregation
  const sql3 = await text2sql.toSql('What is the total revenue by customer?');
  console.log('Query 3:', sql3);
  console.log('Results:', adapter.execute(sql3));
}

demo().catch(console.error);

In-Memory Databases

Perfect for testing:
import Database from 'better-sqlite3';

const db = new Database(':memory:');

// Create test schema
db.exec(`
  CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
  INSERT INTO users (name) VALUES ('Alice'), ('Bob');
`);

const adapter = new Sqlite({
  execute: (sql) => db.prepare(sql).all(),
  grounding: [tables()]
});

const sql = await text2sql.toSql('Show all users');
console.log(sql);
// SELECT * FROM users

Read-Only Databases

Open database in read-only mode:
import Database from 'better-sqlite3';

const db = new Database('./database.db', {
  readonly: true,
  fileMustExist: true
});

const adapter = new Sqlite({
  execute: (sql) => db.prepare(sql).all(),
  grounding: [tables()]
});

Performance Tips

1. Use Indexes

SQLite benefits greatly from indexes:
CREATE INDEX idx_customers_email ON customers(email);
CREATE INDEX idx_orders_created_at ON orders(created_at);

2. Enable WAL Mode

Write-Ahead Logging improves concurrency:
const db = new Database('./database.db');
db.pragma('journal_mode = WAL');

3. Optimize Introspection

Minimize grounding functions for faster startup:
// Fast introspection
grounding: [tables(), info()]

// Slower but more comprehensive
grounding: [
  tables(),
  views(),
  info(),
  indexes(),
  constraints(),
  rowCount(),
  columnStats()
]

4. Prepare Statements

Reuse prepared statements:
const stmts = new Map();

const adapter = new Sqlite({
  execute: (sql) => {
    let stmt = stmts.get(sql);
    if (!stmt) {
      stmt = db.prepare(sql);
      stmts.set(sql, stmt);
    }
    return stmt.all();
  },
  grounding: [tables()]
});

Limitations

No Advanced Constraints

SQLite has limited constraint support:
  • No CHECK constraints with subqueries
  • No partial indexes (before SQLite 3.8)
  • No generated columns (before SQLite 3.31)

Limited ALTER TABLE

SQLite cannot:
  • Drop columns (before SQLite 3.35)
  • Modify column types
  • Add constraints to existing tables

No Stored Procedures

SQLite does not support:
  • Stored procedures
  • User-defined functions (in SQL)
  • Triggers with multiple statements

Best Practices

1. Use Transactions

db.exec('BEGIN');
try {
  // Multiple operations
  db.exec('INSERT INTO ...');
  db.exec('UPDATE ...');
  db.exec('COMMIT');
} catch (error) {
  db.exec('ROLLBACK');
  throw error;
}

2. Close Connections

process.on('exit', () => db.close());
process.on('SIGINT', () => {
  db.close();
  process.exit(0);
});

3. Backup Regularly

import { copyFileSync } from 'fs';

setInterval(() => {
  const timestamp = new Date().toISOString();
  copyFileSync('./database.db', `./backups/database-${timestamp}.db`);
}, 3600000);  // Hourly backups

Next Steps

PostgreSQL Adapter

Setup for PostgreSQL databases

Teachables

Inject domain knowledge

Getting Started

Complete getting started guide

API Reference

Full adapter API documentation

Build docs developers (and LLMs) love