feat: add sqlite storage schema

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-11 20:59:31 +08:00
co-authored by Codex
parent 9ed8579b8e
commit ffec8b5ad7
8 changed files with 137 additions and 7 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ Update this file before each feature commit.
| Scaffold | Completed | initial scaffold commit | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | Rust crate, lint policy, pinned TLS-sensitive dependencies, `.gitignore`, and minimal entry points are in place. |
| API contract docs | Completed | initial scaffold commit | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | `/v1/*` OpenAI-compatible body, `/admin/*` frontend envelope, body codes, camelCase, and request ID policy are documented. |
| Configuration | Completed | pending config commit | `cargo test default_config_keeps_only_codex_backend`; full `fmt/test/clippy` before commit | Defines Codex-only config, default YAML, and `Arc<AppServices>` state shell. |
| SQLite storage | Planned | | | |
| SQLite storage | Completed | pending storage commit | `cargo test migrations_create_accounts_and_event_tables`; full `fmt/test/clippy` before commit | Creates SQLite WAL connector, migrations for accounts/API keys/sessions/cookies/fingerprints/events, and event/account indexes. |
| Admin auth and client API keys | Planned | | | |
| Secret encryption | Planned | | | |
| Logging and pagination | Planned | | | |
@@ -929,7 +929,7 @@ git commit -m "feat: add codex-only configuration"
- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`
- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/storage_migration_test.rs`
- [ ] **Step 1: Write failing migration test**
- [x] **Step 1: Write failing migration test**
Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/storage_migration_test.rs`:
@@ -951,7 +951,7 @@ async fn migrations_create_accounts_and_event_tables() {
}
```
- [ ] **Step 2: Run test and verify RED**
- [x] **Step 2: Run test and verify RED**
Run:
@@ -961,7 +961,7 @@ cargo test migrations_create_accounts_and_event_tables
Expected: compile failure because `storage::db` does not exist.
- [ ] **Step 3: Create migrations**
- [x] **Step 3: Create migrations**
Create `/home/zyy/桌面/Codes/codex-proxy-rs/migrations/0001_initial.sql`:
@@ -1068,7 +1068,7 @@ create index if not exists idx_client_api_keys_prefix on client_api_keys(prefix)
create index if not exists idx_account_cookies_account on account_cookies(account_id);
```
- [ ] **Step 4: Implement database connector**
- [x] **Step 4: Implement database connector**
Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`:
@@ -1103,7 +1103,7 @@ pub async fn connect_sqlite(database_url: &str) -> Result<SqlitePool, sqlx::Erro
}
```
- [ ] **Step 5: Verify migrations**
- [x] **Step 5: Verify migrations**
Run:
@@ -1113,7 +1113,7 @@ cargo test migrations_create_accounts_and_event_tables
Expected: pass.
- [ ] **Step 6: Commit storage**
- [x] **Step 6: Commit storage**
```bash
git add Cargo.toml migrations src/storage src/lib.rs tests/storage_migration_test.rs
+89
View File
@@ -0,0 +1,89 @@
pragma foreign_keys = on;
create table if not exists admin_users (
id text primary key,
password_hash text not null,
created_at text not null,
updated_at text not null
);
create table if not exists admin_sessions (
id text primary key,
user_id text not null references admin_users(id) on delete cascade,
expires_at text not null,
created_at text not null
);
create table if not exists client_api_keys (
id text primary key,
name text not null,
prefix text not null,
key_hash text not null,
enabled integer not null default 1,
created_at text not null,
last_used_at text
);
create table if not exists accounts (
id text primary key,
email text,
account_id text,
user_id text,
label text,
plan_type text,
access_token_cipher text not null,
refresh_token_cipher text,
status text not null,
quota_json text,
quota_fetched_at text,
added_at text not null,
updated_at text not null
);
create table if not exists account_usage (
account_id text primary key references accounts(id) on delete cascade,
request_count integer not null default 0,
input_tokens integer not null default 0,
output_tokens integer not null default 0,
cached_tokens integer not null default 0,
last_used_at text
);
create table if not exists account_cookies (
id text primary key,
account_id text not null references accounts(id) on delete cascade,
domain text not null,
name text not null,
value_cipher text not null,
path text not null default '/',
expires_at text,
updated_at text not null,
unique(account_id, domain, name, path)
);
create table if not exists fingerprints (
id text primary key,
app_version text not null,
build_number text not null,
platform text not null,
arch text not null,
chromium_version text not null,
user_agent_template text not null,
source text not null,
created_at text not null
);
create table if not exists event_logs (
id text primary key,
request_id text,
kind text not null,
level text not null,
account_id text,
route text,
model text,
status_code integer,
latency_ms integer,
message text not null,
metadata_json text not null,
created_at text not null
);
+6
View File
@@ -0,0 +1,6 @@
create index if not exists idx_event_logs_created_id on event_logs(created_at desc, id desc);
create index if not exists idx_event_logs_kind_created on event_logs(kind, created_at desc);
create index if not exists idx_event_logs_request_id on event_logs(request_id);
create index if not exists idx_accounts_status on accounts(status);
create index if not exists idx_client_api_keys_prefix on client_api_keys(prefix);
create index if not exists idx_account_cookies_account on account_cookies(account_id);
+1
View File
@@ -1,3 +1,4 @@
pub mod config;
pub mod error;
pub mod state;
pub mod storage;
+16
View File
@@ -0,0 +1,16 @@
use std::{str::FromStr, time::Duration};
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
pub async fn connect_sqlite(database_url: &str) -> Result<SqlitePool, sqlx::Error> {
let options = SqliteConnectOptions::from_str(database_url)?
.create_if_missing(true)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.foreign_keys(true)
.busy_timeout(Duration::from_secs(5));
// 中文注释:WAL 和外键必须在连接层统一启用,避免后续仓储绕过一致性约束。
let pool = SqlitePool::connect_with(options).await?;
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(pool)
}
+1
View File
@@ -0,0 +1 @@
pub mod db;
+17
View File
@@ -0,0 +1,17 @@
use codex_proxy_rs::storage::db::connect_sqlite;
#[tokio::test]
async fn migrations_create_accounts_and_event_tables() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("test.sqlite");
let url = format!("sqlite://{}", db.display());
let pool = connect_sqlite(&url).await.unwrap();
let row: (i64,) = sqlx::query_as(
"select count(*) from sqlite_master where type = 'table' and name in ('accounts', 'client_api_keys', 'event_logs')",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(row.0, 3);
}