# v0.17.1 — SQLite Backend ## Overview Adds SQLite as an alternative database backend alongside PostgreSQL. Enables single-binary deployments, air-gapped environments, edge nodes, and developer laptops without requiring a PostgreSQL instance. ## Architecture ``` DB_DRIVER env var │ ├─ "postgres" (default) → database.DialectPostgres → postgres.NewStores() └─ "sqlite" → database.DialectSQLite → sqlite.NewStores() Both implement the same 19 store.* interfaces unchanged. ``` ### Key Design Decisions | Decision | Rationale | |----------|-----------| | Parallel `store/sqlite/` package | Clean separation, no runtime branching in SQL | | `?` placeholders (not `$N`) | SQLite native, no conversion overhead | | JSON text arrays (`'["a","b"]'`) | Replace `TEXT[]`/`UUID[]` + `pq.Array` | | `json_each()` for array membership | Replace `= ANY(array_col)` | | App-side `store.NewID()` UUIDs | Replace `DEFAULT gen_random_uuid()` | | `datetime('now')` | Replace `NOW()` / `CURRENT_TIMESTAMP` | | `LIKE` fallback for search | Replace `tsvector` / `ts_rank` | | Feature-gated vector search | pgvector → returns error hint on SQLite | | `excluded.col` in ON CONFLICT | Replace Postgres `$N` reuse in SET clause | | WAL mode + single writer | Optimal SQLite concurrency for web apps | ## Files Delivered ### Infrastructure | File | Purpose | |------|---------| | `server/config/config.go` | Added `DBDriver` field + `DB_DRIVER` env var | | `server/database/dialect.go` | `Dialect` type, `IsPostgres()`, `IsSQLite()` | | `server/database/database.go` | Dual-driver `Connect()`: postgres + sqlite | | `server/database/migrate.go` | Dialect-aware migration runner | | `server/database/migrations/sqlite/001_v017_schema.sql` | Full SQLite schema | | `server/store/id.go` | `store.NewID()` — app-side UUID generation | ### Store Layer (19 stores) | File | Lines | Key Transforms | |------|-------|---------------| | `store/sqlite/helpers.go` | Query builders with `?`, JSON helpers | | `store/sqlite/stores.go` | `NewStores()` constructor | | `store/sqlite/user.go` | `store.NewID()`, `datetime('now')` | | `store/sqlite/provider.go` | JSONB→TEXT, `store.NewID()` | | `store/sqlite/catalog.go` | JSONB→TEXT scanning | | `store/sqlite/persona.go` | `json_each()` for group grants | | `store/sqlite/policy.go` | `excluded.col` in upsert | | `store/sqlite/user_settings.go` | COALESCE with `excluded.col` | | `store/sqlite/channel.go` | Tags as JSON text array | | `store/sqlite/message.go` | `store.NewID()` | | `store/sqlite/note.go` | LIKE search fallback, JSON tags | | `store/sqlite/usage.go` | Dynamic `?` filters | | `store/sqlite/pricing.go` | `excluded.col` upserts | | `store/sqlite/extension.go` | `store.NewID()` | | `store/sqlite/attachment.go` | `store.NewID()` | | `store/sqlite/knowledge_bases.go` | `IN(?)` replaces `ANY()`, vector search gated | | `store/sqlite/groups.go` | `store.NewID()` | | `store/sqlite/resource_grants.go` | `json_each()` for array membership | | `store/sqlite/team.go` | `store.NewID()`, `excluded.col` | | `store/sqlite/audit.go` | `store.NewID()` | | `store/sqlite/global_config.go` | `excluded.col` upsert | ### Integration | File | Purpose | |------|---------| | `server/MAIN_GO_PATCH.md` | Shows exact main.go changes needed | ## Pattern Conversion Reference ### Placeholders ```sql -- Postgres: WHERE id = $1 AND name = $2 -- SQLite: WHERE id = ? AND name = ? ``` ### UUID Generation ```go // Postgres: RETURNING id (gen_random_uuid() DEFAULT) // SQLite: obj.ID = store.NewID() // INSERT INTO table (id, ...) VALUES (?, ...) // RETURNING created_at (id already set) ``` ### Array Types ```go // Postgres: pq.Array(teamIDs) → team_id = ANY($3) // SQLite: for _, tid := range teamIDs { args = append(args, tid) } // team_id IN (?,?,?) // // Postgres: pq.Array(&tags) → stored as TEXT[] // SQLite: ArrayToJSON(tags) → stored as '["a","b"]' // ScanArray(jsonStr) → read back ``` ### Array Membership (granted_groups) ```sql -- Postgres: gm.group_id = ANY(rg.granted_groups) -- SQLite: JOIN json_each(rg.granted_groups) je ON je.value = gm.group_id ``` ### ON CONFLICT Upserts ```sql -- Postgres: ON CONFLICT (key) DO UPDATE SET value = $2, updated_by = $3 -- SQLite: ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_by = excluded.updated_by ``` ### Timestamps ```sql -- Postgres: NOW(), CURRENT_TIMESTAMP, TIMESTAMPTZ -- SQLite: datetime('now'), TEXT (ISO 8601) ``` ### Full-Text Search ```sql -- Postgres: search_vector @@ to_tsquery('english', $1) -- SQLite: (title LIKE ? OR content LIKE ?) -- per word ``` ### Vector Search ``` -- Postgres: c.embedding <=> $1::vector (pgvector cosine distance) -- SQLite: Feature-gated. Returns error hint. -- KB ingestion works (stores text chunks). -- Future: sqlite-vec extension support. ``` ## Configuration ```bash # PostgreSQL (default — no changes needed) DB_DRIVER=postgres DATABASE_URL=postgres://user:pass@host:5432/switchboard # SQLite DB_DRIVER=sqlite DATABASE_URL=switchboard.db # file path DATABASE_URL=:memory: # in-memory (testing) DATABASE_URL=/data/switchboard.db # absolute path ``` Auto-detection: if `DB_DRIVER` is empty, inferred from `DATABASE_URL` format. ## SQLite Pragmas (set automatically) | Pragma | Value | Purpose | |--------|-------|---------| | `journal_mode` | WAL | Concurrent readers | | `busy_timeout` | 5000ms | Retry on lock | | `foreign_keys` | ON | Enforce FK constraints | ## Limitations vs PostgreSQL | Feature | PostgreSQL | SQLite | |---------|-----------|--------| | Vector similarity search | ✅ pgvector | ❌ Feature-gated | | Full-text search | ✅ tsvector/tsquery | ⚠ LIKE fallback | | Concurrent writes | ✅ MVCC | ⚠ Single writer (WAL) | | Array columns | ✅ Native | ⚠ JSON text arrays | | `NUMERIC(12,6)` precision | ✅ Exact | ⚠ REAL (float64) | | GIN indexes | ✅ Native | ❌ Not available | | Partial indexes | ✅ Full support | ✅ Supported | | RETURNING clause | ✅ Full support | ✅ SQLite 3.35+ | ## go.mod Addition ``` require modernc.org/sqlite v1.34.5 // pure-Go, no CGO required ``` ## TODO (follow-up) - [ ] Wire main.go dialect switch (see MAIN_GO_PATCH.md) - [ ] Add `modernc.org/sqlite` to go.mod - [ ] CI matrix: run integration tests against both drivers - [ ] SQLite-specific integration test suite - [ ] sqlite-vec extension support for vector search (optional) - [ ] Benchmark: SQLite vs PostgreSQL for typical workloads