[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-09 21:08:02 +09:00
parent f0befc887a
commit 93ec7e9056
363 changed files with 68333 additions and 64 deletions
+362
View File
@@ -0,0 +1,362 @@
---
id: db-sqlite-patterns
title: SQLite 패턴 — Embedded / WAL / 동시성
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [database, sqlite, embedded, vibe-coding]
tech_stack: { language: "TS / SQL", applicable_to: ["Backend", "Mobile"] }
applied_in: []
aliases: [SQLite, better-sqlite3, libSQL, WAL mode, busy_timeout, embedded DB]
---
# SQLite Patterns
> 가장 사용된 DB. **Single file, embedded, 0 setup**. Mobile / desktop / edge / 작은 web. WAL mode + busy_timeout = 동시성 OK.
## 📖 핵심 개념
- WAL: write-ahead log. read 안 차단.
- Journal mode: DELETE / WAL.
- BEGIN IMMEDIATE: write lock 즉시.
- Vacuum: 빈 공간 회수.
## 💻 코드 패턴
### Node — better-sqlite3 (sync, fast)
```bash
yarn add better-sqlite3
```
```ts
import Database from 'better-sqlite3';
const db = new Database('app.db');
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('busy_timeout = 5000');
// Prepared statement (재사용)
const insertUser = db.prepare('INSERT INTO users (id, email) VALUES (?, ?)');
insertUser.run(uuid(), 'a@b.com');
// Get
const getUser = db.prepare('SELECT * FROM users WHERE id = ?');
const user = getUser.get(id);
// Many
const all = db.prepare('SELECT * FROM users').all();
```
→ Sync API — async overhead 없음. 가장 빠름.
### Node — node:sqlite (Node 22.5+ built-in)
```ts
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('app.db');
db.exec('CREATE TABLE IF NOT EXISTS users (...)');
const r = db.prepare('SELECT * FROM users WHERE id = ?').get(id);
```
### Bun
```ts
import { Database } from 'bun:sqlite';
const db = new Database('app.db');
db.exec('PRAGMA journal_mode = WAL');
const users = db.prepare('SELECT * FROM users').all();
```
### libSQL (Turso fork)
```ts
import { createClient } from '@libsql/client';
const turso = createClient({ url: 'file:app.db' });
await turso.execute('CREATE TABLE ...');
```
→ SQLite + replication + embedded replica.
### WAL mode (필수)
```sql
PRAGMA journal_mode = WAL;
-- Read 가 write 차단 X
-- Concurrent read OK
-- 하지만 single writer
```
```sql
PRAGMA synchronous = NORMAL;
-- WAL + NORMAL = FAST + 보통 안전 (power loss 일부 위험 OK)
-- FULL = 더 안전, 느림
-- OFF = 매우 빠름, 위험
```
### Busy timeout
```sql
PRAGMA busy_timeout = 5000;
-- Write lock 5초 대기 후 SQLITE_BUSY
```
```ts
// 또는 retry loop
for (let i = 0; i < 10; i++) {
try {
db.exec('UPDATE users SET ...');
break;
} catch (e) {
if (e.code === 'SQLITE_BUSY') await sleep(50);
else throw e;
}
}
```
### Transaction
```ts
const tx = db.transaction((users: User[]) => {
for (const u of users) insertUser.run(u.id, u.email);
});
tx(users); // 자동 BEGIN / COMMIT, 실패 시 ROLLBACK
```
→ 1000개 insert = 매 INSERT 보다 100x 빠름.
### Concurrent write (queue)
```ts
import PQueue from 'p-queue';
const writeQueue = new PQueue({ concurrency: 1 });
async function writeUser(u: User) {
return writeQueue.add(() => insertUser.run(u.id, u.email));
}
```
→ Single-writer queue. SQLITE_BUSY 회피.
### BEGIN IMMEDIATE / EXCLUSIVE
```sql
BEGIN IMMEDIATE;
-- 즉시 write lock — 다른 process 가 read OK 그러나 write X
-- 작업
COMMIT;
```
→ 큰 transaction 시 안전.
### Schema
```sql
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'pro')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TEXT
) STRICT; -- type 강제 (3.37+)
CREATE INDEX users_email ON users(email);
CREATE INDEX users_active ON users(created_at) WHERE deleted_at IS NULL;
```
→ STRICT mode 가 type 안전.
### JSON
```sql
CREATE TABLE events (
id TEXT PRIMARY KEY,
data TEXT, -- JSON
created_at TEXT
);
INSERT INTO events VALUES ('1', json('{"key": "value"}'), datetime('now'));
-- Query
SELECT * FROM events WHERE json_extract(data, '$.key') = 'value';
-- Index on JSON
CREATE INDEX events_key ON events(json_extract(data, '$.key'));
```
### Full-text search (FTS5)
```sql
CREATE VIRTUAL TABLE docs_fts USING fts5(title, body);
INSERT INTO docs_fts (title, body) VALUES ('Hello', 'World');
SELECT * FROM docs_fts WHERE docs_fts MATCH 'world';
SELECT * FROM docs_fts WHERE docs_fts MATCH '"exact phrase"';
SELECT *, bm25(docs_fts) AS score FROM docs_fts WHERE docs_fts MATCH 'foo' ORDER BY score;
```
### Vector search (sqlite-vss / vec)
```sql
CREATE VIRTUAL TABLE vss_demo USING vss0(
embedding(1536)
);
INSERT INTO vss_demo (embedding) VALUES (?);
SELECT rowid, distance FROM vss_demo
WHERE vss_search(embedding, vss_search_params(?, 10));
```
### Backup
```ts
// Online backup
db.backup('backup.db', { progress: ({ totalPages, remainingPages }) => {
console.log(`${100 * (totalPages - remainingPages) / totalPages}%`);
} });
```
```bash
# 또는 sqlite3 CLI
sqlite3 app.db ".backup backup.db"
# 또는 file copy + WAL (위험 — WAL checkpoint 후)
sqlite3 app.db "PRAGMA wal_checkpoint(FULL)"
cp app.db backup.db
```
### VACUUM
```sql
VACUUM;
-- DELETE 후 빈 공간 정리
-- 큰 작업 — 한 번 lock
```
```sql
PRAGMA auto_vacuum = INCREMENTAL;
PRAGMA incremental_vacuum;
-- 점진 vacuum
```
### EXPLAIN
```sql
EXPLAIN QUERY PLAN
SELECT * FROM users WHERE email = 'a@b.com';
-- SCAN TABLE users (bad — table scan)
-- SEARCH TABLE users USING INDEX users_email (good)
```
### Foreign keys (default off!)
```sql
PRAGMA foreign_keys = ON; -- 매 connection 필수
CREATE TABLE orders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
### Mobile (iOS / Android)
```swift
// iOS GRDB
import GRDB
let dbQueue = try DatabaseQueue(path: "app.db")
try dbQueue.write { db in
try User(id: ..., email: ...).insert(db)
}
```
```kotlin
// Android — Room (위 [[Android_Room_Patterns]])
@Database(entities = [User::class], version = 1)
abstract class AppDb : RoomDatabase()
```
### Use case
```
- Mobile app
- Desktop app
- Embedded device / IoT
- 작은 web (single user)
- Test fixture
- Local cache
- Edge worker (D1, Cloudflare)
- Single-process server (read-heavy)
```
### Server use case (놀라운)
```
"SQLite 가 production server 가능?"
가능. 단:
- Single writer (queue)
- Same machine (no network)
- 작은 / 중간 (TB 미만)
- LiteFS / Litestream 으로 replication
```
```bash
# Litestream — S3 backup + replica
litestream replicate -config litestream.yml
```
### Common pitfalls
```
1. WAL 안 활성: lock 자주.
2. busy_timeout 0: SQLITE_BUSY 자주.
3. Foreign keys off (default): 의도 안 됨.
4. Concurrent writer 다중: 큰 lock.
5. Transaction 없는 batch INSERT: 매 commit fsync.
6. Big TEXT / BLOB: vacuum 비싸.
7. Migration breaks: TEXT type drift.
```
### Cross-process locking
```
같은 file 다중 process — OS file lock.
WAL = read 다중 OK + 1 writer.
NFS / network filesystem: SQLite 권장 X.
```
### libSQL vs SQLite
```
libSQL = SQLite fork (Turso).
+ Replication (sync to remote)
+ HTTP API
+ TLS
+ 일부 extension built-in (vss)
대부분 호환.
```
## 🤔 의사결정 기준
| 환경 | 추천 |
|---|---|
| Mobile | Native (Room / GRDB) |
| Desktop | better-sqlite3 / node:sqlite |
| Edge | D1 / libSQL / Turso |
| Local dev | better-sqlite3 |
| 작은 server | SQLite + Litestream |
| 큰 / 다중 writer | Postgres |
| Analytic | DuckDB |
## ❌ 안티패턴
- **WAL 안 활성**: lock 지옥.
- **busy_timeout 0**: BUSY 자주.
- **Foreign keys off**: 의도 깨짐.
- **No transaction batch insert**: 100x 느림.
- **NFS / 네트워크 file**: corruption 위험.
- **VACUUM prod 큰 table**: lock — INCREMENTAL.
- **Backup 으로 file cp**: WAL 미반영 — sqlite3 backup.
## 🤖 LLM 활용 힌트
- WAL + busy_timeout + foreign_keys 항상.
- better-sqlite3 가 빠름 (sync).
- Transaction 으로 batch.
- Litestream 가 server SQLite + backup.
## 🔗 관련 문서
- [[DB_DuckDB_Embedded]]
- [[DB_Serverless_Edge]]
- [[Android_Room_Patterns]]