Skip to main content

helix_core/effect/
storage.rs

1/// 存储操作(具体 SQL 由 driver 的 Storage 实现构造)
2#[derive(Debug, Clone)]
3pub enum StorageOp {
4    /// batch upsert:INSERT ... ON CONFLICT(conflict_key) DO UPDATE ...
5    BatchUpsert(UpsertSpec),
6    /// 通用单调 MAX-guard 写:仅当 new value > 当前值才写(CAS / 单调寄存器语义,严格 >)。
7    MonotonicUpsert(MonotonicUpsertSpec),
8    /// batch update:UPDATE table SET patch WHERE key_col IN (key_vals)
9    BatchUpdate(BatchUpdateSpec),
10    /// 守卫式自增:`UPDATE … SET bump_col = bump_col + delta[, set…] WHERE key=? AND ? > guard_col`。
11    /// 通用计数器原语(无 read-modify-write),见 `GuardedBumpSpec`。
12    GuardedBump(GuardedBumpSpec),
13    /// 复合作用域守卫式自增:在 scope + key 精确单行内完成幂等计数与绝对列写。
14    ScopedGuardedBump(ScopedGuardedBumpSpec),
15    /// get:SELECT * FROM table WHERE ...(单行)
16    Get(GetSpec),
17    /// 复合作用域 get:SELECT * FROM table WHERE scope=? AND key=?(单行)
18    ScopedGet(ScopedGetSpec),
19    /// 作用域集合内的聚合最大值读取;空集合必须返回空结果,禁止退化为全表查询。
20    ScopedMax(ScopedMaxSpec),
21    /// 作用域集合内的多行读取;空集合必须返回空结果,不能退化为全表查询。
22    ScopedScan(ScopedScanSpec),
23    /// scan:SELECT * FROM table [LIMIT n](多行,C3)
24    Scan(ScanSpec),
25    /// batch delete:`DELETE FROM table WHERE scope_col = ? AND key_col IN (key_vals)`。
26    /// 复合 PK 作用域删除(如 channel_member 成员离场:scope=channel_id + key=user_id)。
27    /// O(k) 单语句;scope 等值约束**绝不跨作用域误删**。见 `BatchDeleteSpec`。
28    BatchDelete(BatchDeleteSpec),
29}
30
31/// 稀疏、非 SQL 存储适配器使用的表身份描述。
32///
33/// 具体表名由业务模块提供;平台驱动只据此建立通用索引,不依赖业务类型或解析 DDL。
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct SparseTableSpec {
36    pub table: &'static str,
37    pub identity_cols: &'static [&'static str],
38    pub scope_col: Option<&'static str>,
39}
40
41/// BatchUpsert 规格
42///
43/// ## MAJ-5 修复
44///
45/// `table` 和 `conflict_key` 改为 `&'static str`,消除热路径的运行时 String 分配。
46/// 表名和冲突键都是编译期已知的常量(示例 "message"、"temporary_id"——core 不解释含义),
47/// `"message".to_string()` 每次消息落库都产生一次堆分配,改为静态引用零成本。
48///
49/// 注意:`conflict_key` 的具体值(如 `"temporary_id"`)由 ACL-1 在 helix-im 侧提供,
50/// helix-core 只传递字符串,不理解其含义。
51#[derive(Debug, Clone)]
52pub struct UpsertSpec {
53    /// 目标表名(编译期常量,如 "message")
54    pub table: &'static str,
55    pub rows: Vec<Row>,
56    /// ON CONFLICT 的列名(如 ACL-1 提供 "temporary_id";core 不知道含义)
57    pub conflict_key: Option<&'static str>,
58    /// 冲突时**不**更新的列(INSERT 仍写其字面值,ON CONFLICT DO UPDATE 排除它们)。
59    ///
60    /// 用途:守卫列 / 本地维护列——新行用提供的字面值,但已存在的行保留既有值
61    /// ("server 缺省 → 回退本地"语义在 upsert 层表达,无需先读后写)。列名编译期常量,
62    /// driver 不解释含义(守 HX-C001)。空 = 旧行为(除 conflict_key 外全列更新)。
63    ///
64    /// 默认空:用 `UpsertSpec::new(...)` 构造可零改动迁移既有调用方。
65    pub exclude_from_update: Vec<&'static str>,
66    /// 可选的冲突行更新守卫;仅原行该列 `IS expected` 时允许 `DO UPDATE`。
67    ///
68    /// 守卫值走绑定参数,不进入 SQL 文本。缺行时仍按普通 INSERT 处理,因此调用方可用
69    /// `SqlValue::Null` 表达“没有旧版本”的并发插入守卫。带守卫的规格只允许一个 row。
70    pub update_guard: Option<UpsertGuard>,
71    /// Optional orderable column: skip conflicting rows whose stored version is greater.
72    /// Values must have a consistent SQL type; missing/NULL stored versions admit initialization.
73    /// With an update_guard, a skipped row instead returns a conflict and rolls back the transaction.
74    pub version_column: Option<&'static str>,
75}
76
77/// batch upsert 冲突行的原子更新守卫。
78///
79/// `column` 是编译期列名;`expected` 是绑定到 `ON CONFLICT ... DO UPDATE ... WHERE`
80/// 的运行时值。驱动必须比较冲突前的原行,而不是 `excluded` 新值。
81#[derive(Debug, Clone)]
82pub struct UpsertGuard {
83    /// 冲突原行中参与 `IS` 比较的列名。
84    pub column: &'static str,
85    /// 冲突原行必须满足的值;`SqlValue::Null` 表示 SQL NULL。
86    pub expected: SqlValue,
87}
88
89impl UpsertSpec {
90    /// 便捷构造(无排除列,旧默认行为)——既有调用方零改动迁移点。
91    pub fn new(table: &'static str, rows: Vec<Row>, conflict_key: Option<&'static str>) -> Self {
92        Self {
93            table,
94            rows,
95            conflict_key,
96            exclude_from_update: Vec::new(),
97            version_column: None,
98            update_guard: None,
99        }
100    }
101}
102
103/// 通用单调 MAX-guard 写规格:仅当 `value > 当前值` 才写入(CAS / 单调寄存器语义)。
104///
105/// 通用原语,core 不含任何业务含义——典型用途:增量同步水位、presence 心跳时间戳、单调计数器。
106/// `scope_key` 是运行时值(由上层提供,如 channel_id.to_string()),core 不解析其含义。
107///
108/// ## 表名 / 列名作数据(HX-C001)
109///
110/// `table` / `key_col` / `value_col` / `touch_col` 都是**编译期常量**(`&'static str`),
111/// 由上层业务模块提供。core 只把它们当字符串透传给 driver 拼 SQL,**绝不**理解其业务含义
112/// ——业务 schema 不进 core(守 HX-C001)。`touch_col` 为 `Some` 时,driver 用自身的
113/// ambient 时钟(driver 层合法)写穿该列为当前毫秒时间戳(非权威镜像列,不参与 MAX guard 判定)。
114#[derive(Debug, Clone)]
115pub struct MonotonicUpsertSpec {
116    /// 目标表名(编译期常量;core 不解释含义)
117    pub table: &'static str,
118    /// PRIMARY KEY 列名(编译期常量)
119    pub key_col: &'static str,
120    /// 单调 MAX-guard 的值列名(编译期常量)
121    pub value_col: &'static str,
122    /// 可选「最近更新时间」列名;`Some` 时 driver 写穿当前时间戳,
123    /// 不参与 MAX guard。`None` = 无此列(最小单调寄存器形态)。
124    pub touch_col: Option<&'static str>,
125    /// PK 运行时值(由上层提供);core 不解析其含义。
126    pub scope_key: String,
127    /// 单调写入值;仅当 `> 当前值` 才前进。
128    pub value: i64,
129}
130
131/// batch_update 规格
132///
133/// MAJ-5:`table` 和 `key_col` 改为 `&'static str`(编译期常量)
134#[derive(Debug, Clone)]
135pub struct BatchUpdateSpec {
136    pub table: &'static str,
137    pub key_col: &'static str,
138    pub key_vals: Vec<SqlValue>,
139    pub patch: Row,
140}
141
142/// BatchDelete 规格:`DELETE FROM table WHERE scope_col = ? AND key_col IN (key_vals)`。
143///
144/// 复合 PK 作用域删除(如 channel_member:scope=channel_id 等值 + key=user_id IN 列表)——
145/// 表名 / 列名编译期常量(core 不解释含义,守 HX-C001),单语句 O(k) 删 k 行。scope 等值约束
146/// **绝不跨作用域误删**(无 scope 会把该 user 从所有 channel 删掉)。`key_vals` 空 → 上层应不产 op。
147#[derive(Debug, Clone)]
148pub struct BatchDeleteSpec {
149    /// 目标表名(编译期常量)
150    pub table: &'static str,
151    /// 作用域列名(等值约束,编译期常量)—— 复合 PK 第一段,如 channel_id。
152    pub scope_col: &'static str,
153    /// 作用域运行时值(定位作用域;core 不解析含义)
154    pub scope_val: SqlValue,
155    /// IN-list 列名(编译期常量)—— 复合 PK 第二段,如 user_id。
156    pub key_col: &'static str,
157    /// 待删行的 key 运行时值列表
158    pub key_vals: Vec<SqlValue>,
159}
160
161/// 守卫式自增规格:单行原子计数器递增 + 可选绝对列写,带前进守卫——**无 read-modify-write**。
162///
163/// SQL 语义(表名 / 列名都是编译期常量,由上层业务模块提供——守 HX-C001,core/driver
164/// 不解释含义;`key_val` / `set_cols` 值 / `bump_delta` / `guard_val` 是运行时数据):
165/// ```sql
166/// UPDATE <table> SET <bump_col> = <bump_col> + <bump_delta> [, <set_cols…> = ?…]
167///   WHERE <key_col> = ? AND ? > <guard_col>
168/// ```
169/// (守卫占位 `?` 绑定 `guard_val`:仅当 `guard_val > 当前 guard_col` 才命中更新。)
170///
171/// ## 为什么需要独立原语(不能用 `BatchUpdate`)
172///
173/// `BatchUpdate` 只能 `SET col = ?`(绝对值)+ `WHERE key IN (…)`——无法表达
174/// `col = col + delta`(自增)也无法表达 `AND ? > guard_col`(前进守卫)。把「读出 +1 写回」
175/// 下沉成单条 SQL `col = col + delta` 是 HX-C005 热路径 O(1) 的硬约束(禁应用层 RMW);
176/// 守卫条件让「同一消息重复投递不重复 +1」「乱序旧消息不污染计数」在 SQL 层幂等成立。
177///
178/// ## 通用性(YAGNI 边界)
179///
180/// 纯计数器语义,core 不含任何业务含义——典型用途:未读计数 +1(guard=last_post_at 防回退)、
181/// 限流桶累加、引用计数。`set_cols` 为空 = 纯自增;`guard_val=i64::MIN`/无意义守卫由上层避免
182/// (上层须保证 guard 语义正确)。`set_cols` 列名同样是 `&'static str` 编译期常量。
183#[derive(Debug, Clone)]
184pub struct GuardedBumpSpec {
185    /// 目标表名(编译期常量;core 不解释含义)
186    pub table: &'static str,
187    /// 主键列名(编译期常量)
188    pub key_col: &'static str,
189    /// 主键运行时值(定位单行;core 不解析含义)
190    pub key_val: SqlValue,
191    /// 被自增的数值列名(编译期常量)
192    pub bump_col: &'static str,
193    /// 自增量(运行时值,通常 +1;负数 = 自减)
194    pub bump_delta: i64,
195    /// 同一前进守卫下的额外计数列自增。
196    ///
197    /// 用于 unread/mention/urgent 等多个派生计数必须在同一条新消息守卫下同时推进的场景。
198    /// 列名仍是编译期常量;delta 使用绑定参数,不参与 SQL 字符串拼接。
199    pub extra_bumps: Vec<(&'static str, i64)>,
200    /// 与自增同事务写入的绝对值列(列名编译期常量,值运行时);空 = 纯自增。
201    pub set_cols: Row,
202    /// 前进守卫列名(编译期常量):仅当 `guard_val > <guard_col> 当前值` 才更新整行。
203    pub guard_col: &'static str,
204    /// 守卫比较值(运行时):严格大于现值才命中(防重复 / 旧值回退)。
205    pub guard_val: i64,
206}
207
208/// 复合作用域守卫式自增规格,避免复合主键热路径退化为应用层 read-modify-write。
209#[derive(Debug, Clone)]
210pub struct ScopedGuardedBumpSpec {
211    /// 目标表名(编译期常量;core 不解释业务 schema)。
212    pub table: &'static str,
213    /// 第一维作用域列名。
214    pub scope_col: &'static str,
215    /// 第一维作用域运行时值。
216    pub scope_val: SqlValue,
217    /// 第二维键列名。
218    pub key_col: &'static str,
219    /// 第二维键运行时值。
220    pub key_val: SqlValue,
221    /// 被自增的数值列。
222    pub bump_col: &'static str,
223    /// 自增量;0 表示只应用同一守卫下的绝对列 patch。
224    pub bump_delta: i64,
225    /// 与计数器同一 SQL 更新的绝对列。
226    pub set_cols: Row,
227    /// 前进守卫列。
228    pub guard_col: &'static str,
229    /// 仅严格大于当前守卫值时更新。
230    pub guard_val: i64,
231    /// 可选的附加等值谓词;仅所有守卫同时满足时更新。
232    ///
233    /// 这是通用存储条件,用于 legacy 写路径保护已存在的 versioned projection;core
234    /// 不解释业务字段含义,driver 只负责把该条件安全地绑定到 SQL/内存谓词。
235    pub additional_guard: Option<(&'static str, SqlValue)>,
236}
237
238/// get 规格(单行;MAJ-5:`table`/`key_col` = `&'static str`)。
239#[derive(Debug, Clone)]
240pub struct GetSpec {
241    pub table: &'static str,
242    pub key_col: &'static str,
243    pub key_val: SqlValue,
244}
245
246/// 复合作用域 get 规格,用两个等值键 O(1) 定位复合主键行。
247#[derive(Debug, Clone)]
248pub struct ScopedGetSpec {
249    pub table: &'static str,
250    pub scope_col: &'static str,
251    pub scope_val: SqlValue,
252    pub key_col: &'static str,
253    pub key_val: SqlValue,
254}
255
256/// 作用域集合内的最大值查询规格。
257///
258/// `scope_col`、`value_col` 和 `result_alias` 都是编译期列名;driver 只负责参数绑定与
259/// 聚合,不解释表或列的业务含义。空 `scope_values` 表示无可信作用域,driver 应返回空结果。
260#[derive(Debug, Clone)]
261pub struct ScopedMaxSpec {
262    pub table: &'static str,
263    pub scope_col: &'static str,
264    pub scope_values: Vec<SqlValue>,
265    pub value_col: &'static str,
266    pub result_alias: &'static str,
267}
268
269/// 作用域集合内的有界多行查询规格。
270///
271/// driver 必须先应用 `scope_col IN scope_values` 再执行 limit;若匹配行数超过 limit,
272/// 必须返回错误而不是静默截断。`limit` 是调用方明确给出的硬上限,空作用域返回空数组。
273/// `scope_values` 按集合解释:重复值不得让同一行跨分块重复返回。
274#[derive(Debug, Clone)]
275pub struct ScopedScanSpec {
276    pub table: &'static str,
277    pub scope_col: &'static str,
278    pub scope_values: Vec<SqlValue>,
279    pub limit: usize,
280}
281
282/// scan 规格:通用多行扫描读(C3)。on_start 载入单调水位(全表)/ P6 投影查询(等值过滤+排序)。
283/// 过滤/排序**可选 additive**(HX-C004):`filter` 为 None、`order_by` 为空 = 旧全表语义
284/// (零行为改变);列名仍为编译期常量(HX-C001),方向由结构化枚举表达,禁止调用方拼 SQL。
285#[derive(Debug, Clone)]
286pub struct ScanSpec {
287    /// 目标表名(编译期常量)
288    pub table: &'static str,
289    /// 最多返回行数(None = 全表;driver 应设硬上限防御)
290    pub limit: Option<u32>,
291    /// 可选等值过滤 `WHERE <col> = <val>`(列名编译期常量,值运行时)。None = 不过滤(旧全表)。
292    pub filter: Option<(&'static str, SqlValue)>,
293    /// 结构化排序键,按切片顺序形成 `ORDER BY`;空切片 = 不排序(旧 rowid 序)。
294    pub order_by: &'static [ScanOrder],
295}
296
297/// Scan 排序方向。driver 只把枚举映射成固定 SQL 关键字,不接受运行时 SQL 片段。
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum SortDirection {
300    Asc,
301    Desc,
302}
303
304/// 单个 Scan 排序键。列名是编译期常量,业务层可安全组合多列确定序。
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub struct ScanOrder {
307    pub column: &'static str,
308    pub direction: SortDirection,
309}
310
311impl ScanOrder {
312    pub const fn asc(column: &'static str) -> Self {
313        Self {
314            column,
315            direction: SortDirection::Asc,
316        }
317    }
318
319    pub const fn desc(column: &'static str) -> Self {
320        Self {
321            column,
322            direction: SortDirection::Desc,
323        }
324    }
325}
326
327/// SQL 值类型(不含数据库具体类型)
328#[derive(Debug, Clone)]
329pub enum SqlValue {
330    Text(String),
331    Integer(i64),
332    Real(f64),
333    /// 二进制大对象(M2:无损携带,禁 BLOB→lossy text 静默损坏)
334    Blob(Vec<u8>),
335    Null,
336}
337
338/// 行数据(列名 → SQL 值)
339pub type Row = Vec<(String, SqlValue)>;