Files
raisfast/docs/database-compatibility.md
T

11 KiB
Raw Blame History

数据库兼容设计

架构概览

项目通过 编译时 feature flag 选择唯一的数据库后端,所有差异封装在 src/db/ 模块内。 业务代码(models / services / handlers / content_type)完全不感知底层数据库类型。

src/db/                          ← 唯一知道数据库差异的地方
  driver.rs    ← sealed trait DbDriver + 3 个 impl(核心)
  pool.rs      ← 7 个类型别名(Pool, DbRow, DbArguments...)
  sql_type.rs  ← 12 种 SQL 列类型映射表
  connection.rs← 连接池初始化 + schema 执行
  backup.rs    ← 备份逻辑
  tenant.rs    ← 租户隔离辅助
  schema.rs    ← 编译时 schema SQL(build.rs 生成)

src/macros.rs                   ← define_enum! 宏(sqlx Type/Decode/Encode)
raisfast-derive/src/schema.rs   ← proc-macro 编译时 Dialect 枚举
migrations/                     ← 每个数据库独立的 SQL 文件
  sqlite/schema.sqlite.sql
  postgres/schema.postgres.sql
  mysql/schema.mysql.sql

核心设计模式

1. Sealed Trait — DbDriver

src/db/driver.rs 定义了一个 sealed trait,编译时只有唯一的 impl 被激活:

pub struct Sqlite;
pub struct Postgres;
pub struct MySql;

mod sealed { pub trait Sealed {} }
impl sealed::Sealed for Sqlite {}
impl sealed::Sealed for Postgres {}
impl sealed::Sealed for MySql {}

#[cfg(feature = "db-sqlite")]
pub type Driver = Sqlite;          // 编译时选定

pub trait DbDriver: sealed::Sealed {
    fn pk_type() -> &'static str;           // "INTEGER" / "BIGINT"
    fn ph(idx: usize) -> String;            // "?" / "$1"
    fn now_fn() -> &'static str;            // "strftime(...)" / "NOW()"
    fn ago_expr(days: i64) -> String;
    fn date_trunc_day(col: &str) -> String;
    fn upsert_clause(...) -> String;
    fn excluded_col(col: &str) -> String;
    fn returning_clause() -> &'static str;
    fn returning_col(col: &str) -> String;
    fn insert_ignore_sql(...) -> String;
    fn columns_sql(table: &str) -> (String, usize);
    fn column_names_sql(table: &str) -> (String, usize);
    fn rebuild_wrapper_sql(...) -> String;
    fn has_column(pool, table, column) -> impl Future<Output = bool> + Send;
    fn table_exists(pool, table) -> impl Future<Output = bool> + Send;
    fn list_user_tables(pool, excluded) -> impl Future<Output = Vec<String>> + Send;
    fn fetch_columns_with_types(pool, table) -> impl Future<Output = Result<...>> + Send;
}

业务代码调用方式:

use crate::db::{DbDriver, Driver};

let sql = format!("SELECT * FROM users WHERE id = {}", Driver::ph(1));
let pk = Driver::pk_type();                     // "INTEGER PRIMARY KEY"
let exists = Driver::table_exists(pool, "users").await;

Driver 是编译时类型别名,DbDriver 是 sealed trait — 编译器直接单态化,零运行时开销。

2. 数据表驱动 — SqlType

src/db/sql_type.rs 用 const 数组替代 match 分支:

pub enum SqlType { Varchar, Text, Integer, BigInt, Real, Boolean, Blob,
                   Timestamp, Date, Time, Decimal, Json }

impl SqlType {
    pub fn as_str(self) -> &'static str {
        TYPE_MAP[self as usize]       // 数组下标,编译时确定
    }
}

#[cfg(feature = "db-sqlite")]
const TYPE_MAP: [&str; 12] = row("TEXT", "TEXT", "INTEGER", "INTEGER", ...);

#[cfg(feature = "db-postgres")]
const TYPE_MAP: [&str; 12] = row("VARCHAR(255)", "TEXT", "INTEGER", "BIGINT", ...);

#[cfg(feature = "db-mysql")]
const TYPE_MAP: [&str; 12] = row("VARCHAR(255)", "TEXT", "INT", "BIGINT", ...);

加新数据库只需加一行 const TYPE_MAP。

3. 内部宏 — define_db_types!

src/db/pool.rs 用宏消除 7 × 3 = 21 个 cfg 块的重复:

macro_rules! define_db_types {
    ($db_type:path {
        Pool = $pool:ty, Transaction = $tx:ty, Row = $row:ty,
        Connection = $conn:ty, Arguments = $args:ty,
        QueryResult = $result:ty, PoolConnection = $pc:ty,
    }) => { /* 定义 7 个 type alias */ };
}

#[cfg(all(feature = "db-sqlite", not(...)))]
define_db_types!(sqlx::Sqlite { Pool = sqlx::SqlitePool, ... });

4. CRUD 宏系统

raisfast-derive 中的 crud_find! / crud_update! 等 166 处调用自动根据编译时 DATABASE_URL 检测 dialect,生成正确的 SQL。业务代码完全不需要改动。

5. define_enum! 子宏

src/macros.rs 中的 define_enum! 自动为枚举生成 sqlx 的 Type / Decode / Encode impl, 通过 __define_enum_sqlx_impl! 子宏参数化数据库类型。


添加新数据库支持(完整步骤)

以添加 CockroachDB 为例(与 PostgreSQL 高度兼容)。

Step 1: Cargo.toml — 加 feature flag

[features]
db-cockroach = ["sqlx/postgres"]    # CockroachDB 使用 postgres 协议

# 互斥检查在 pool.rs 的 compile_error! 中处理
# 需要在 pool.rs 和各 cfg 条件中加上 db-cockroach

Step 2: pool.rs — 加类型别名(7 行)

在现有 PostgreSQL 块后面加:

#[cfg(all(
    feature = "db-cockroach",
    not(any(feature = "db-sqlite", feature = "db-postgres", feature = "db-mysql"))
))]
define_db_types!(sqlx::Postgres {           // CockroachDB 用 postgres 驱动
    Pool = sqlx::PgPool,
    Transaction = sqlx::Transaction<'a, sqlx::Postgres>,
    Row = sqlx::postgres::PgRow,
    Connection = sqlx::postgres::PgConnection,
    Arguments = sqlx::postgres::PgArguments<'q>,
    QueryResult = sqlx::postgres::PgQueryResult,
    PoolConnection = sqlx::pool::PoolConnection<sqlx::Postgres>,
});

同时更新所有 not(any(...)) 条件,加上 feature = "db-cockroach"。

Step 3: driver.rs — 加 impl DbDriver(~100 行)

pub struct Cockroach;
impl sealed::Sealed for Cockroach {}

#[cfg(feature = "db-cockroach")]
pub type Driver = Cockroach;

#[cfg(feature = "db-cockroach")]
impl DbDriver for Cockroach {
    fn pk_type() -> &'static str { "BIGINT PRIMARY KEY" }
    fn fk_type() -> &'static str { "BIGINT" }
    fn ph(idx: usize) -> String { format!("${idx}") }
    fn now_fn() -> &'static str { "NOW()" }
    fn ago_expr(days: i64) -> String { format!("NOW() - INTERVAL '{days} days'") }
    fn date_trunc_day(col: &str) -> String { format!("DATE_TRUNC('day', {col}::timestamp)") }
    fn upsert_clause(conflict_cols: &str, assignments: &str) -> String {
        format!("ON CONFLICT({conflict_cols}) DO UPDATE SET {assignments}")
    }
    fn excluded_col(col: &str) -> String { format!("excluded.{col}") }
    fn returning_clause() -> &'static str { "RETURNING *" }
    fn returning_col(col: &str) -> String { format!("RETURNING {col}") }
    fn insert_ignore_sql(table: &str, columns: &str, phs: &str) -> String {
        assert!(is_safe_identifier(table), "unsafe table name: {table}");
        format!("INSERT INTO {table} ({columns}) VALUES ({phs}) ON CONFLICT DO NOTHING")
    }
    fn columns_sql(table: &str) -> (String, usize) { /* 同 PostgreSQL */ }
    fn column_names_sql(table: &str) -> (String, usize) { /* 同 PostgreSQL */ }
    fn rebuild_wrapper_sql(...) -> String { /* 同 PostgreSQL */ }
    async fn has_column(...) -> bool { /* 同 PostgreSQL */ }
    async fn table_exists(...) -> bool { /* 同 PostgreSQL */ }
    async fn list_user_tables(...) -> Vec<String> { /* 同 PostgreSQL */ }
    async fn fetch_columns_with_types(...) -> Result<...> { /* 同 PostgreSQL */ }
}

提示:如果是与现有数据库兼容的(如 CockroachDB ↔ PostgreSQL), 直接复制对应的 impl 修改差异即可。

Step 4: sql_type.rs — 加一行数据(1 行)

#[cfg(feature = "db-cockroach")]
const TYPE_MAP: Row = row(
    "VARCHAR(255)",     // Varchar
    "TEXT",             // Text
    "INTEGER",          // Integer
    "BIGINT",           // BigInt
    "DOUBLE PRECISION", // Real
    "BOOLEAN",          // Boolean
    "BYTEA",            // Blob
    "TIMESTAMPTZ(0)",   // Timestamp
    "DATE",             // Date
    "TIMETZ",           // Time
    "NUMERIC(16,4)",    // Decimal
    "JSONB",            // Json
);

Step 5: macros.rs — 加一个 cfg 分支(5 行)

在 __define_enum_sqlx! 宏中加:

#[cfg(feature = "db-cockroach")]
$crate::__define_enum_sqlx_impl! {
    $name,
    db = sqlx::Postgres,
    type_info = sqlx::postgres::PgTypeInfo,
    value_ref = sqlx::postgres::PgValueRef<'_>,
    arg_buf = sqlx::postgres::PgArgumentBuffer,
}

Step 6: schema.rs (proc-macro) — 加 Dialect 变体(5 行)

pub enum Dialect {
    Sqlite,
    Postgres,
    Mysql,
    Cockroach,           // 新增
}

// 在 const TABLE 中加:
DialectCfg {
    migration_dir: "cockroach",
    schema_ext: ".cockroach.sql",
    ph_prefix: "$",
},

// 在 from_env() 中加:
} else if url.starts_with("cockroach:") || url.starts_with("postgresql://...cockroach") {
    Dialect::Cockroach

Step 7: connection.rs — 加连接初始化(~10 行)

#[cfg(feature = "db-cockroach")]
{
    use sqlx::pool::PoolOptions;
    let pool = PoolOptions::<sqlx::Postgres>::new()
        .max_connections(pool_size)
        .min_connections(1)
        .acquire_timeout(ACQUIRE_TIMEOUT)
        .idle_timeout(Some(IDLE_TIMEOUT))
        .max_lifetime(Some(MAX_LIFETIME))
        .connect(database_url)
        .await?;
    tracing::info!(%pool_size, "cockroach connection pool initialized");
    Ok(pool)
}

Step 8: migrations/ — 加 SQL 文件

migrations/cockroach/
  schema.cockroach.sql
  tenantable.cockroach.sql

Step 9: config/app.rs — 加默认 URL(可选)

#[cfg(feature = "db-cockroach")]
fn default_db_url() -> String {
    "cockroach://root@localhost:26257/raisfast?sslmode=disable".to_string()
}

Step 10: 编译验证

SQLX_OFFLINE=false DATABASE_URL="cockroach://..." \
  cargo clippy --tests --no-default-features --features "db-cockroach,plugin-js,plugin-lua,plugin-rhai" \
  -- -D warnings

SQLX_OFFLINE=false DATABASE_URL="cockroach://..." \
  cargo test --no-default-features --features "db-cockroach,plugin-js,plugin-lua,plugin-rhai"

改动清单汇总

# 文件 改动 行数
1 Cargo.toml feature flag + 依赖 ~2
2 src/db/pool.rs define_db_types! 块 + 更新互斥条件 ~10
3 src/db/driver.rs marker struct + impl DbDriver ~100
4 src/db/sql_type.rs const TYPE_MAP 数据行 ~15
5 src/macros.rs __define_enum_sqlx! cfg 分支 ~5
6 raisfast-derive/src/schema.rs Dialect 变体 + DialectCfg + from_env ~5
7 src/db/connection.rs 连接池初始化 ~15
8 migrations/xxx/ schema SQL 文件 1 文件
9 src/config/app.rs 默认 URL(可选) ~3
业务代码(48 个文件) 零改动 0

设计原则

  1. 编译时保证 — compile_error! 确保恰好选一个后端,错误配置直接编译失败
  2. Sealed trait — 外部 crate 无法实现 DbDriver,所有差异集中在 src/db/
  3. 数据驱动 — SqlType 和 Dialect 用 const 数据表,加新条目无需理解逻辑
  4. 零运行时开销 — 所有分支在编译时单态化,没有 if cfg 运行时判断
  5. CRUD 宏优先 — 166 处数据库调用通过宏自动适配,加新 DB 不改业务代码
  6. 业务层零 cfg — #[cfg(feature = "db-xxx")] 只出现在 src/db/ 和 src/macros.rs