SQLwrite is a SQLite extension that translates natural language queries to SQL using LLMs.
sqlwrite.cpp- Main extension code, registersask()andsqlwrite()SQL functionsaistream.hpp- Stream-based interface for LLM interactions with retry logicllm_backend.hpp- Abstraction layer supporting multiple LLM providersshell.c- Modified SQLite shell with auto-load supportopenai.hpp- Legacy OpenAI client (kept for reference, no longer used directly)
When compiled with -DSQLWRITE_AUTOLOAD:
- Extension is automatically loaded when database opens
- Uses regular SQLite API (
sqlite3.h) - No need for
.loadcommand
When compiled without SQLWRITE_AUTOLOAD:
- Loadable extension for any SQLite shell
- Uses extension API (
sqlite3ext.h+SQLITE_EXTENSION_INIT1/2) - Requires
.load ./sqlwritecommand
shell.cmodified to callsqlite3_sqlwrite_init()inopen_db()whenSQLWRITE_AUTOLOADdefinedsqlwrite.cppconditionally includessqlite3.hvssqlite3ext.hbased on build mode- Both modes built by CMake:
sqlwrite-bin(built-in) andsqlwrite.so(extension) - Shell binary always uses bundled
shell.c+sqlite3.cto ensure version match
- OpenAI - Uses GPT-4o via chat completions API
- AWS Bedrock - Uses Claude 3 Sonnet via Bedrock runtime API
Priority order:
- AWS credentials (env vars or
~/.aws/credentials) → Bedrock OPENAI_API_KEYenv var → OpenAI
- URI paths must be URL-encoded in the canonical request (e.g., colons become
%3A) - Service name is
bedrock(notbedrock-runtime) - Host is
bedrock-runtime.{region}.amazonaws.com
- Claude 3 Sonnet (
anthropic.claude-3-sonnet-20240229-v1:0) supports on-demand throughput - Newer Claude 4 models require inference profiles - not supported in on-demand mode
- Model ID goes in the URL path:
/model/{model_id}/invoke
Claude has strict message format rules:
- First message MUST be "user" role
- Messages must alternate between "user" and "assistant"
- System prompts go in the
systemfield, not as messages
The existing codebase uses "assistant" role for system prompts (OpenAI pattern). The backend handles this by:
- Treating "assistant" messages before any "user" message as system content
- Merging consecutive "user" messages into one
- Properly alternating roles in the final message array
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 4096,
"system": "...",
"messages": [{"role": "user", "content": "..."}]
}- Environment variables take priority:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY - Falls back to
~/.aws/credentialsfile - Important:
AWS_SESSION_TOKENis also read from environment - if set but doesn't match the access key, authentication fails - When using explicit credentials, run
unset AWS_SESSION_TOKENfirst
RETRY_ON_EMPTY_RESULTS=1- Retries with fuzzy match hints if query returns no resultsRETRY_ON_TOO_MANY_RESULTS=0- Disabled - was causing bad translations by telling LLM to constrain queries that legitimately return many results (e.g., "show all artists")
- Code samples random rows from database to give LLM context
- Different samples can lead to different interpretations
- This is expected LLM behavior
- "first message must use the user role" - Claude requires user message first; backend handles this
- "roles must alternate" - Consecutive same-role messages; backend merges them
- "on-demand throughput isn't supported" - Model requires inference profile; use Claude 3 models
- Signature mismatch - Check URI encoding in canonical request
- "sqlite3_close() returns 5" - Unfinalized prepared statements; ensure all
sqlite3_prepare_v2have matchingsqlite3_finalize - AWS_SESSION_TOKEN mismatch - Unset the token when using explicit access key/secret
- Bizarre query constraints - Was caused by
RETRY_ON_TOO_MANY_RESULTS; now disabled
# Build
cmake -B build && cmake --build build
# Test with Bedrock
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-east-1
unset AWS_SESSION_TOKEN
cd build && LD_LIBRARY_PATH=. ./sqlwrite-bin ../test/test.db
# Test query
sqlite> select ask('show me all artists');