mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
## Why Local message boards use a fixed database filename. Callers need separate storage for boards that share a session identity. ## What changed - Add `LocalBoardOptions` with a validated `storage_namespace`, and support it when opening, configuring, and permanently deleting boards. - Allow `LocalBoard::with_options` to select storage for its connections. - Store custom namespaces in `message_board_<namespace>_1.sqlite`, while preserving `agent_message_board_1.sqlite` for the default namespace. - Have the agent message board extension select the same namespace for opening, setup, and deletion. ## Testing Add coverage for namespace isolation, reopening saved posts, and avoiding collisions with other runtime databases. Update setup tests to exercise a custom namespace through reopening and deletion. GitOrigin-RevId: a6ea8d171809625edeedc3a8ac3ee2e1bbf1682a
99 lines
3.9 KiB
Rust
99 lines
3.9 KiB
Rust
//! Owns the local database schema and pool registry shared by runtime handles and setup.
|
|
//! Initialization and deletion recovery share the registry lock to exclude stale pools.
|
|
|
|
use super::options::LocalBoardOptions;
|
|
use super::storage_error;
|
|
use codex_protocol::error::Result;
|
|
use codex_state::SqliteConfig;
|
|
use codex_state::SqlitePoolWriteExt;
|
|
use sqlx::SqlitePool;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::LazyLock;
|
|
use std::sync::Weak;
|
|
use tokio::sync::Mutex;
|
|
|
|
// Weak entries let the last board handle release its pool. Initialization and
|
|
// recovery share one lock so concurrent starts cannot open duplicate or stale pools.
|
|
pub(super) static POOLS: LazyLock<Mutex<HashMap<PathBuf, Weak<SqlitePool>>>> =
|
|
LazyLock::new(Mutex::default);
|
|
|
|
pub(super) const SCHEMA: &str = "
|
|
CREATE TABLE IF NOT EXISTS board_templates (board TEXT PRIMARY KEY NOT NULL, template TEXT NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS deleted_boards (board TEXT PRIMARY KEY NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
board TEXT NOT NULL, name TEXT NOT NULL, name_search TEXT NOT NULL, created_at TEXT NOT NULL, timestamp INTEGER NOT NULL, author TEXT NOT NULL, description TEXT,
|
|
PRIMARY KEY(board,name)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
board TEXT NOT NULL, id TEXT NOT NULL, channel TEXT NOT NULL, root TEXT NOT NULL,
|
|
author TEXT NOT NULL, timestamp INTEGER NOT NULL, body_search TEXT NOT NULL,
|
|
payload TEXT NOT NULL, request_id TEXT NOT NULL, request TEXT NOT NULL,
|
|
UNIQUE(board,id), UNIQUE(board,request_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS posts_board_channel ON posts(board,channel,seq);
|
|
CREATE INDEX IF NOT EXISTS posts_board_channel_timestamp ON posts(board,channel,timestamp,seq);
|
|
CREATE INDEX IF NOT EXISTS posts_roots_created ON posts(board,channel,timestamp,seq) WHERE id=root;
|
|
CREATE INDEX IF NOT EXISTS posts_board_root ON posts(board,root,seq);
|
|
CREATE INDEX IF NOT EXISTS posts_board_root_timestamp ON posts(board,root,timestamp,seq);
|
|
CREATE INDEX IF NOT EXISTS posts_board_timestamp ON posts(board,timestamp,seq);
|
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
board TEXT NOT NULL, target TEXT NOT NULL, agent TEXT NOT NULL,
|
|
PRIMARY KEY(board,target,agent)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS subscription_opt_outs (
|
|
board TEXT NOT NULL, target TEXT NOT NULL, agent TEXT NOT NULL,
|
|
PRIMARY KEY(board,target,agent)
|
|
);";
|
|
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "serialize shared pool initialization"
|
|
)]
|
|
pub(super) async fn open(
|
|
sqlite: &SqliteConfig,
|
|
options: &LocalBoardOptions,
|
|
) -> Result<Arc<SqlitePool>> {
|
|
let filename = options.database_file()?;
|
|
tokio::fs::create_dir_all(sqlite.home()).await?;
|
|
let path = tokio::fs::canonicalize(sqlite.home()).await?.join(filename);
|
|
let mut pools = POOLS.lock().await;
|
|
pools.retain(|_, pool| pool.strong_count() > 0);
|
|
let pool = if let Some(pool) = pools
|
|
.get(&path)
|
|
.and_then(Weak::upgrade)
|
|
.filter(|pool| !pool.is_closed())
|
|
{
|
|
pool
|
|
} else {
|
|
let pool = sqlite
|
|
.open_read_write_pool(&path)
|
|
.await
|
|
.map_err(storage_error)?;
|
|
let mut tx = pool.begin_immediate().await.map_err(storage_error)?;
|
|
sqlx::raw_sql(SCHEMA)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.map_err(storage_error)?;
|
|
let has_description: bool = sqlx::query_scalar(
|
|
"SELECT EXISTS(SELECT 1 FROM pragma_table_info('channels') WHERE name='description')",
|
|
)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.map_err(storage_error)?;
|
|
if !has_description {
|
|
sqlx::query("ALTER TABLE channels ADD COLUMN description TEXT")
|
|
.execute(&mut *tx)
|
|
.await
|
|
.map_err(storage_error)?;
|
|
}
|
|
tx.commit().await.map_err(storage_error)?;
|
|
let pool = Arc::new(pool);
|
|
pools.insert(path, Arc::downgrade(&pool));
|
|
pool
|
|
};
|
|
Ok(pool)
|
|
}
|