Skip to main content

rust_store_core/dialect/
row.rs

1//! 平铺 JOIN 行 → 嵌套 Mongo 文档还原
2
3use std::collections::HashMap;
4
5use serde_json::{Map, Value};
6
7use super::ir::{RowCol, RowShape};
8
9/// 把一组平铺行(`Vec<Map<String, Value>>`)按 row_shape 还原为嵌套 Mongo 文档数组。
10///
11/// 规则:
12/// - 标量列(`is_array=false`)写入其 `json_path` 指定的嵌套位置;
13/// - 关系列(`is_array=true`)按 `json_path` 逐级归并:每一级按该关系的主键分组去重,
14///   基数由 `ones[level]` 决定(`true`→对象或 `null`,`false`→数组)。
15pub fn restore_rows(shape: &RowShape, rows: &[Value]) -> Value {
16    // 先按「根标量键」分组,同根聚为一篇文档;无根键列(如 `$group` 分组结果,
17    // 其输出没有 `_id` 列)→ 每行独立成篇(分组结果每行本就是一个文档)。
18    // 用 HashMap 索引根键 → 桶下标(`buckets` 保持插入顺序),避免线性 `find` 的 O(行数 × 根数)。
19    let mut index: HashMap<Value, usize> = HashMap::new();
20    let mut buckets: Vec<Vec<&Value>> = Vec::new();
21    for (i, row) in rows.iter().enumerate() {
22        let key = root_key(shape, row).unwrap_or_else(|| Value::from(i as u64));
23        match index.get(&key) {
24            Some(&idx) => buckets[idx].push(row),
25            None => {
26                index.insert(key, buckets.len());
27                buckets.push(vec![row]);
28            }
29        }
30    }
31
32    let cols: Vec<&RowCol> = shape.columns.iter().collect();
33    let docs: Vec<Value> = buckets
34        .into_iter()
35        .map(|group| {
36            // 每行「显式存在字段集合」(`__present` 哨兵,缺失 vs null 三态);
37            // 仅作用于根层标量(哨兵列来自根表)
38            let present = shape
39                .present_alias
40                .as_deref()
41                .and_then(|a| row_val(group[0], a).as_str())
42                .map(|s| s.to_owned());
43            Value::Object(build_object(&cols, &group, 0, present.as_deref()))
44        })
45        .collect();
46    Value::Array(docs)
47}
48
49/// 由若干平铺行构造**一层**对象:
50/// - `path_len` 是本层的 `json_path` 前缀长度(根 = 0,`lessons` 内 = 1,…);
51/// - 标量列(`json_path.len() == path_len + 1`,或**非关系列**的多段点号路径,
52///   如 `$group` 的 by 键 `meta.level`)取首行值写入(点号路径还原为嵌套对象);
53/// - 更深的**关系列**(`is_array`,`json_path` 更深)= 本级关系,按关系名分组递归(`relation_value`)。
54fn build_object(
55    cols: &[&RowCol],
56    rows: &[&Value],
57    path_len: usize,
58    present: Option<&str>,
59) -> Map<String, Value> {
60    let mut obj = Map::new();
61    for c in cols {
62        if c.json_path.len() == path_len + 1 || (!c.is_array && c.json_path.len() > path_len + 1) {
63            let val = row_val(rows[0], &c.alias).clone();
64            merge_path(
65                &mut obj,
66                &c.json_path[path_len..],
67                val,
68                present,
69                c.always,
70                c.is_bool,
71            );
72        }
73    }
74    // 本级关系名(仅关系列 `is_array`,保留出现顺序)
75    let mut rels: Vec<String> = Vec::new();
76    for c in cols {
77        if c.is_array && c.json_path.len() > path_len + 1 {
78            let r = &c.json_path[path_len];
79            if !rels.iter().any(|x| x == r) {
80                rels.push(r.clone());
81            }
82        }
83    }
84    for r in rels {
85        let sub: Vec<&RowCol> = cols
86            .iter()
87            .copied()
88            .filter(|c| {
89                c.is_array && c.json_path.len() > path_len + 1 && c.json_path[path_len] == r
90            })
91            .collect();
92        obj.insert(r.clone(), relation_value(&sub, rows, path_len));
93    }
94    obj
95}
96
97/// 还原 `path_len` 这一级的关系值:按该关系主键分组去重后递归构造子文档。
98/// 基数 `ones[path_len]`:`true` → 至多一条(对象或 `null`),`false` → 数组。
99fn relation_value(cols: &[&RowCol], rows: &[&Value], path_len: usize) -> Value {
100    let one = cols
101        .first()
102        .and_then(|c| c.ones.get(path_len).copied())
103        .unwrap_or_else(|| cols.first().map(|c| c.one).unwrap_or(false));
104    let id_col = cols
105        .iter()
106        .find(|c| c.json_path.len() == path_len + 2 && c.json_path[path_len + 1] == "_id");
107    // 每个 SQL 行 = 一个 (父, 子) 对;多关系 LEFT JOIN 会形成笛卡尔积,同一子行重复出现。
108    // 按子文档主键分组(Mongo `$lookup` 每个子文档只出现一次),无主键列时退化为逐行独立。
109    let mut order: Vec<String> = Vec::new();
110    let mut groups: HashMap<String, Vec<&Value>> = HashMap::new();
111    for row in rows {
112        // 该关系在本行完全缺失(LEFT JOIN 全 NULL)→ 不是子文档
113        if cols.iter().all(|c| row_val(row, &c.alias).is_null()) {
114            continue;
115        }
116        let key = match id_col.map(|c| row_val(row, &c.alias)) {
117            Some(v) if !v.is_null() => v.to_string(),
118            _ => format!("\u{0}{}", order.len()),
119        };
120        if !groups.contains_key(&key) {
121            order.push(key.clone());
122        }
123        groups.entry(key).or_default().push(row);
124    }
125    let docs: Vec<Value> = order
126        .into_iter()
127        .map(|k| Value::Object(build_object(cols, &groups[&k], path_len + 1, None)))
128        .collect();
129    if one {
130        docs.into_iter().next().unwrap_or(Value::Null)
131    } else {
132        Value::Array(docs)
133    }
134}
135
136/// 从行取值(按 alias 直接取列)
137fn row_val<'a>(row: &'a Value, alias: &str) -> &'a Value {
138    match row {
139        Value::Object(map) => map.get(alias).unwrap_or(&Value::Null),
140        _ => &Value::Null,
141    }
142}
143
144/// 根分组键:根表 `_id` 标量列(`json_path == ["_id"]`)。无该列(如 `$group`
145/// 分组结果)→ `None`,调用方按行独立成篇。
146fn root_key(shape: &RowShape, row: &Value) -> Option<Value> {
147    let id_alias = shape
148        .columns
149        .iter()
150        .find(|c| !c.is_array && c.json_path.len() == 1 && c.json_path[0] == "_id")?
151        .alias
152        .clone();
153    row.get(&id_alias).cloned()
154}
155
156/// 把 value 写到 obj 的嵌套路径(创建中间 object)
157///
158/// 缺失 vs null 三态(F-07/H-01):标量值为 null 时,仅当字段在该行「显式存在集合」
159/// (`present`)中才写入 `key: null`,否则(字段缺失)不产出该键 —— 对齐 Mongo
160/// 「显式 null 有键、缺失无键」。`present == None` 表示本语句未查哨兵列(count/关系聚合),
161/// 退化为旧语义:null 一律不产出键。
162///
163/// `always`(§9.2(2) 归一聚合计算列):为 `true` 时即使 `null` 也强制写入,
164/// 与 Mongo `$addFields` 对空集聚合产出显式 `null`(§9.7)逐行对齐。
165///
166/// `is_bool`(§9.7 布尔归一):为 `true` 时先把 SQL 的 `0/1` 归一为 JSON `bool`,
167/// 使「同一逻辑布尔字段」在 MySQL/SQLite(`TINYINT(1)` / `INTEGER`)与
168/// PostgreSQL(原生 `BOOLEAN`)、MongoDB(`true/false`)间**类型一致**。
169fn merge_path(
170    obj: &mut Map<String, Value>,
171    path: &[String],
172    val: Value,
173    present: Option<&str>,
174    always: bool,
175    is_bool: bool,
176) {
177    let val = if is_bool { to_json_bool(val) } else { val };
178    if path.len() <= 1 {
179        if let Some(k) = path.first() {
180            if always || !val.is_null() || present_contains(present, k) {
181                obj.insert(k.clone(), val);
182            }
183        }
184        return;
185    }
186    let head = path[0].clone();
187    let entry = obj
188        .entry(head.clone())
189        .or_insert_with(|| Value::Object(Map::new()));
190    if let Value::Object(m) = entry {
191        merge_path(m, &path[1..], val, present, always, false);
192    }
193}
194
195/// §9.7 布尔归一:`1/0`(数值或数值字符串)→ `true/false`;其余(含 `null`、已是 bool、
196/// PG 原生 bool)原样透传 —— 绝不把非 0/1 的值静默改判为布尔。
197fn to_json_bool(v: Value) -> Value {
198    let as_num = match &v {
199        Value::Number(n) => n.as_f64(),
200        Value::String(s) => s.parse::<f64>().ok(),
201        _ => None,
202    };
203    match as_num {
204        Some(1.0) => Value::Bool(true),
205        Some(0.0) => Value::Bool(false),
206        _ => v,
207    }
208}
209
210/// `present` 集合形如 `,field1,field2,`,判定 `key` 是否显式存在(用 `,key,` 匹配)
211fn present_contains(present: Option<&str>, key: &str) -> bool {
212    match present {
213        Some(s) => s.contains(&format!(",{},", key)),
214        None => false,
215    }
216}
217
218/// 便捷序列化:把 `rowShape + rows` 两步合成一次调用(绑定层常用)
219pub fn restore_rows_json(shape: &Value, rows: &Value) -> Result<Value, String> {
220    let shape = RowShape::from_value(shape)?;
221    let rows = match rows {
222        Value::Array(a) => a.clone(),
223        _ => Vec::new(),
224    };
225    Ok(restore_rows(&shape, &rows))
226}
227
228impl RowShape {
229    /// 从绑定层传入的 JSON 解析(与 `to_value` 对称)
230    pub fn from_value(v: &Value) -> Result<RowShape, String> {
231        let obj = v
232            .get("columns")
233            .and_then(|c| c.as_array())
234            .ok_or("rowShape 缺少 columns")?;
235        let mut columns = Vec::new();
236        for c in obj {
237            let alias = c
238                .get("alias")
239                .and_then(|a| a.as_str())
240                .ok_or("列缺 alias")?
241                .to_string();
242            let path: Vec<String> = c
243                .get("path")
244                .and_then(|p| p.as_array())
245                .map(|arr| {
246                    arr.iter()
247                        .filter_map(|s| s.as_str().map(String::from))
248                        .collect()
249                })
250                .unwrap_or_default();
251            let is_array = c.get("isArray").and_then(|b| b.as_bool()).unwrap_or(false);
252            let one = c.get("one").and_then(|b| b.as_bool()).unwrap_or(false);
253            // 每级关系基数:新契约显式携带 `ones`;旧 shape 缺省时按顶层 `one` 退化
254            let ones: Vec<bool> = match c.get("ones").and_then(|o| o.as_array()) {
255                Some(a) => a.iter().map(|b| b.as_bool().unwrap_or(false)).collect(),
256                None if is_array => vec![one],
257                None => Vec::new(),
258            };
259            let sub_shape = c
260                .get("subShape")
261                .and_then(|s| {
262                    if s.is_null() {
263                        None
264                    } else {
265                        Some(RowShape::from_value(s))
266                    }
267                })
268                .transpose()?;
269            let always = c.get("always").and_then(|b| b.as_bool()).unwrap_or(false);
270            let is_bool = c.get("bool").and_then(|b| b.as_bool()).unwrap_or(false);
271            columns.push(RowCol {
272                alias,
273                json_path: path,
274                is_array,
275                one,
276                ones,
277                sub_shape,
278                always,
279                is_bool,
280            });
281        }
282        let present_alias = v
283            .get("present")
284            .and_then(|p| p.as_str())
285            .filter(|s| !s.is_empty())
286            .map(|s| s.to_string());
287        Ok(RowShape {
288            columns,
289            present_alias,
290        })
291    }
292}