Skip to content

Commit ed8d87f

Browse files
committed
refactor(nodedb): split value.rs, alter.rs, and point.rs into submodules
value.rs (509 lines) → value/ with assignments, convert, defaults, msgpack_write, rows, and time_range submodules. alter.rs (441 lines) → alter/ with add_column, alter_type, drop_column, enforcement, materialized_sum, and rename_column submodules. DDL router schema.rs gains explicit routing for ADD COLUMN, DROP COLUMN, and RENAME COLUMN so requests reach the handlers in the new module layout. point.rs (696 lines) → point/ with apply_put, delete, get, put, and update submodules. All files remain under 200 lines. No behaviour changes.
1 parent 32f33c3 commit ed8d87f

25 files changed

Lines changed: 2134 additions & 1138 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! UPDATE assignment serialization: `(field, SqlExpr)` pairs → wire-ready
2+
//! `UpdateValue` payloads.
3+
//!
4+
//! Literal RHS is pre-encoded as msgpack. Non-literal RHS (arithmetic,
5+
//! functions, CASE, concatenation, ...) is converted to the shared evaluator
6+
//! type `bridge::expr_eval::SqlExpr` and shipped to the Data Plane, where
7+
//! it is evaluated against the current row at apply time.
8+
9+
use nodedb_sql::types::SqlExpr;
10+
11+
use crate::bridge::physical_plan::UpdateValue;
12+
13+
use super::super::expr::sql_expr_to_bridge_expr;
14+
use super::convert::sql_value_to_msgpack;
15+
16+
pub(crate) fn assignments_to_update_values(
17+
assignments: &[(String, SqlExpr)],
18+
) -> crate::Result<Vec<(String, UpdateValue)>> {
19+
let mut result = Vec::with_capacity(assignments.len());
20+
for (field, expr) in assignments {
21+
let value = match expr {
22+
SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)),
23+
_ => UpdateValue::Expr(sql_expr_to_bridge_expr(expr)),
24+
};
25+
result.push((field.clone(), value));
26+
}
27+
Ok(result)
28+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Conversions from `nodedb_sql::types::SqlValue` into runtime / wire forms.
2+
3+
use nodedb_sql::types::SqlValue;
4+
5+
use super::msgpack_write::write_msgpack_value;
6+
7+
pub(crate) fn sql_value_to_nodedb_value(v: &SqlValue) -> nodedb_types::Value {
8+
match v {
9+
SqlValue::Int(i) => nodedb_types::Value::Integer(*i),
10+
SqlValue::Float(f) => nodedb_types::Value::Float(*f),
11+
SqlValue::String(s) => nodedb_types::Value::String(s.clone()),
12+
SqlValue::Bool(b) => nodedb_types::Value::Bool(*b),
13+
SqlValue::Null => nodedb_types::Value::Null,
14+
SqlValue::Array(arr) => {
15+
nodedb_types::Value::Array(arr.iter().map(sql_value_to_nodedb_value).collect())
16+
}
17+
SqlValue::Bytes(b) => nodedb_types::Value::Bytes(b.clone()),
18+
}
19+
}
20+
21+
pub(crate) fn sql_value_to_string(v: &SqlValue) -> String {
22+
match v {
23+
SqlValue::String(s) => s.clone(),
24+
SqlValue::Int(i) => i.to_string(),
25+
SqlValue::Float(f) => f.to_string(),
26+
SqlValue::Bool(b) => b.to_string(),
27+
_ => String::new(),
28+
}
29+
}
30+
31+
pub(crate) fn sql_value_to_bytes(v: &SqlValue) -> Vec<u8> {
32+
match v {
33+
SqlValue::String(s) => s.as_bytes().to_vec(),
34+
SqlValue::Bytes(b) => b.clone(),
35+
SqlValue::Int(i) => i.to_string().as_bytes().to_vec(),
36+
_ => sql_value_to_string(v).into_bytes(),
37+
}
38+
}
39+
40+
/// Encode a SQL value as standard msgpack for field-level updates.
41+
pub(crate) fn sql_value_to_msgpack(v: &SqlValue) -> Vec<u8> {
42+
let mut buf = Vec::with_capacity(16);
43+
write_msgpack_value(&mut buf, v);
44+
buf
45+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! Column DEFAULT expression evaluation at insert time.
2+
//!
3+
//! Supports ID generation functions (UUIDv4/v7, ULID, CUID2, NANOID), `NOW()`,
4+
//! and literal values. More complex defaults (arbitrary expressions) go
5+
//! through the shared SqlExpr evaluator path.
6+
7+
pub(crate) fn evaluate_default_expr(expr: &str) -> Option<nodedb_types::Value> {
8+
let upper = expr.trim().to_uppercase();
9+
match upper.as_str() {
10+
"UUID_V7" | "UUIDV7" | "GEN_UUID_V7()" | "UUID_V7()" => {
11+
Some(nodedb_types::Value::String(nodedb_types::id_gen::uuid_v7()))
12+
}
13+
"UUID_V4" | "UUIDV4" | "UUID" | "GEN_UUID_V4()" | "UUID_V4()" => {
14+
Some(nodedb_types::Value::String(nodedb_types::id_gen::uuid_v4()))
15+
}
16+
"ULID" | "GEN_ULID()" | "ULID()" => {
17+
Some(nodedb_types::Value::String(nodedb_types::id_gen::ulid()))
18+
}
19+
"CUID2" | "CUID2()" => Some(nodedb_types::Value::String(nodedb_types::id_gen::cuid2())),
20+
"NANOID" | "NANOID()" => Some(nodedb_types::Value::String(nodedb_types::id_gen::nanoid())),
21+
"NOW()" => {
22+
let now = std::time::SystemTime::now()
23+
.duration_since(std::time::UNIX_EPOCH)
24+
.unwrap_or_default();
25+
Some(nodedb_types::Value::String(
26+
chrono::DateTime::from_timestamp_millis(now.as_millis() as i64)
27+
.map(|dt| dt.to_rfc3339())
28+
.unwrap_or_else(|| now.as_millis().to_string()),
29+
))
30+
}
31+
_ => parse_parametric_or_literal(expr, &upper),
32+
}
33+
}
34+
35+
fn parse_parametric_or_literal(expr: &str, upper: &str) -> Option<nodedb_types::Value> {
36+
// NANOID(N) — custom length.
37+
if upper.starts_with("NANOID(") && upper.ends_with(')') {
38+
let len_str = &upper[7..upper.len() - 1];
39+
if let Ok(len) = len_str.parse::<usize>() {
40+
return Some(nodedb_types::Value::String(
41+
nodedb_types::id_gen::nanoid_with_length(len),
42+
));
43+
}
44+
}
45+
// CUID2(N) — custom length.
46+
if upper.starts_with("CUID2(") && upper.ends_with(')') {
47+
let len_str = &upper[6..upper.len() - 1];
48+
if let Ok(len) = len_str.parse::<usize>() {
49+
return Some(nodedb_types::Value::String(
50+
nodedb_types::id_gen::cuid2_with_length(len),
51+
));
52+
}
53+
}
54+
// Numeric literal.
55+
if let Ok(i) = expr.trim().parse::<i64>() {
56+
return Some(nodedb_types::Value::Integer(i));
57+
}
58+
if let Ok(f) = expr.trim().parse::<f64>() {
59+
return Some(nodedb_types::Value::Float(f));
60+
}
61+
// Quoted string literal.
62+
let trimmed = expr.trim();
63+
if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
64+
|| (trimmed.starts_with('"') && trimmed.ends_with('"'))
65+
{
66+
return Some(nodedb_types::Value::String(
67+
trimmed[1..trimmed.len() - 1].to_string(),
68+
));
69+
}
70+
None
71+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//! Value conversion utilities: SqlValue ↔ nodedb_types::Value, msgpack encoding,
2+
//! column default evaluation, and WHERE-clause time-range extraction.
3+
4+
pub(super) mod assignments;
5+
pub(super) mod convert;
6+
pub(super) mod defaults;
7+
pub(super) mod msgpack_write;
8+
pub(super) mod rows;
9+
pub(super) mod time_range;
10+
11+
pub(super) use assignments::assignments_to_update_values;
12+
pub(super) use convert::{
13+
sql_value_to_bytes, sql_value_to_msgpack, sql_value_to_nodedb_value, sql_value_to_string,
14+
};
15+
pub(super) use defaults::evaluate_default_expr;
16+
pub(super) use msgpack_write::{
17+
row_to_msgpack, write_msgpack_array_header, write_msgpack_map_header, write_msgpack_str,
18+
write_msgpack_value,
19+
};
20+
pub(super) use rows::rows_to_msgpack_array;
21+
pub(super) use time_range::extract_time_range;
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//! Standard-msgpack writers for `SqlValue`.
2+
//!
3+
//! These are the *only* msgpack producers used by the DML path — no JSON or
4+
//! zerompk intermediary. Format matches the on-wire layout read by
5+
//! `json_from_msgpack` and the Data Plane row decoders.
6+
7+
use nodedb_sql::types::SqlValue;
8+
9+
pub(crate) fn row_to_msgpack(row: &[(String, SqlValue)]) -> crate::Result<Vec<u8>> {
10+
let mut buf = Vec::with_capacity(row.len() * 32);
11+
write_msgpack_map_header(&mut buf, row.len());
12+
for (key, val) in row {
13+
write_msgpack_str(&mut buf, key);
14+
write_msgpack_value(&mut buf, val);
15+
}
16+
Ok(buf)
17+
}
18+
19+
pub(crate) fn write_msgpack_map_header(buf: &mut Vec<u8>, len: usize) {
20+
if len < 16 {
21+
buf.push(0x80 | len as u8);
22+
} else if len <= u16::MAX as usize {
23+
buf.push(0xDE);
24+
buf.extend_from_slice(&(len as u16).to_be_bytes());
25+
} else {
26+
buf.push(0xDF);
27+
buf.extend_from_slice(&(len as u32).to_be_bytes());
28+
}
29+
}
30+
31+
pub(crate) fn write_msgpack_array_header(buf: &mut Vec<u8>, len: usize) {
32+
if len < 16 {
33+
buf.push(0x90 | len as u8);
34+
} else if len <= u16::MAX as usize {
35+
buf.push(0xDC);
36+
buf.extend_from_slice(&(len as u16).to_be_bytes());
37+
} else {
38+
buf.push(0xDD);
39+
buf.extend_from_slice(&(len as u32).to_be_bytes());
40+
}
41+
}
42+
43+
pub(crate) fn write_msgpack_str(buf: &mut Vec<u8>, s: &str) {
44+
let bytes = s.as_bytes();
45+
let len = bytes.len();
46+
if len < 32 {
47+
buf.push(0xA0 | len as u8);
48+
} else if len <= u8::MAX as usize {
49+
buf.push(0xD9);
50+
buf.push(len as u8);
51+
} else if len <= u16::MAX as usize {
52+
buf.push(0xDA);
53+
buf.extend_from_slice(&(len as u16).to_be_bytes());
54+
} else {
55+
buf.push(0xDB);
56+
buf.extend_from_slice(&(len as u32).to_be_bytes());
57+
}
58+
buf.extend_from_slice(bytes);
59+
}
60+
61+
pub(crate) fn write_msgpack_value(buf: &mut Vec<u8>, val: &SqlValue) {
62+
match val {
63+
SqlValue::Null => buf.push(0xC0),
64+
SqlValue::Bool(true) => buf.push(0xC3),
65+
SqlValue::Bool(false) => buf.push(0xC2),
66+
SqlValue::Int(i) => write_msgpack_int(buf, *i),
67+
SqlValue::Float(f) => {
68+
buf.push(0xCB);
69+
buf.extend_from_slice(&f.to_be_bytes());
70+
}
71+
SqlValue::String(s) => write_msgpack_str(buf, s),
72+
SqlValue::Array(arr) => {
73+
write_msgpack_array_header(buf, arr.len());
74+
for item in arr {
75+
write_msgpack_value(buf, item);
76+
}
77+
}
78+
SqlValue::Bytes(b) => write_msgpack_bin(buf, b),
79+
}
80+
}
81+
82+
fn write_msgpack_int(buf: &mut Vec<u8>, i: i64) {
83+
if (0..=127).contains(&i) {
84+
buf.push(i as u8);
85+
} else if (-32..0).contains(&i) {
86+
buf.push(i as u8); // negative fixint
87+
} else if i >= i8::MIN as i64 && i <= i8::MAX as i64 {
88+
buf.push(0xD0);
89+
buf.push(i as i8 as u8);
90+
} else if i >= i16::MIN as i64 && i <= i16::MAX as i64 {
91+
buf.push(0xD1);
92+
buf.extend_from_slice(&(i as i16).to_be_bytes());
93+
} else if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
94+
buf.push(0xD2);
95+
buf.extend_from_slice(&(i as i32).to_be_bytes());
96+
} else {
97+
buf.push(0xD3);
98+
buf.extend_from_slice(&i.to_be_bytes());
99+
}
100+
}
101+
102+
fn write_msgpack_bin(buf: &mut Vec<u8>, b: &[u8]) {
103+
let len = b.len();
104+
if len <= u8::MAX as usize {
105+
buf.push(0xC4);
106+
buf.push(len as u8);
107+
} else if len <= u16::MAX as usize {
108+
buf.push(0xC5);
109+
buf.extend_from_slice(&(len as u16).to_be_bytes());
110+
} else {
111+
buf.push(0xC6);
112+
buf.extend_from_slice(&(len as u32).to_be_bytes());
113+
}
114+
buf.extend_from_slice(b);
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
121+
#[test]
122+
fn row_to_msgpack_produces_standard_format() {
123+
let row = vec![
124+
("count".to_string(), SqlValue::Int(1)),
125+
("label".to_string(), SqlValue::String("homepage".into())),
126+
];
127+
let bytes = row_to_msgpack(&row).unwrap();
128+
assert_eq!(
129+
bytes[0], 0x82,
130+
"expected fixmap(2), got 0x{:02X}. bytes={bytes:?}",
131+
bytes[0]
132+
);
133+
let json = nodedb_types::json_from_msgpack(&bytes).unwrap();
134+
let obj = json.as_object().unwrap();
135+
assert_eq!(obj["count"], 1);
136+
assert_eq!(obj["label"], "homepage");
137+
}
138+
139+
#[test]
140+
fn write_msgpack_value_int() {
141+
let mut buf = Vec::new();
142+
write_msgpack_value(&mut buf, &SqlValue::Int(42));
143+
assert_eq!(buf, vec![42]);
144+
}
145+
146+
#[test]
147+
fn write_msgpack_value_string() {
148+
let mut buf = Vec::new();
149+
write_msgpack_value(&mut buf, &SqlValue::String("hi".into()));
150+
assert_eq!(buf, vec![0xA2, b'h', b'i']);
151+
}
152+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//! Batch-of-rows msgpack encoding for columnar INSERT paths.
2+
3+
use nodedb_sql::types::SqlValue;
4+
5+
use super::convert::sql_value_to_nodedb_value;
6+
use super::defaults::evaluate_default_expr;
7+
8+
pub(crate) fn rows_to_msgpack_array(
9+
rows: &[&Vec<(String, SqlValue)>],
10+
column_defaults: &[(String, String)],
11+
) -> crate::Result<Vec<u8>> {
12+
let arr: Vec<nodedb_types::Value> = rows
13+
.iter()
14+
.map(|row| {
15+
let mut map = std::collections::HashMap::new();
16+
for (key, val) in row.iter() {
17+
map.insert(key.clone(), sql_value_to_nodedb_value(val));
18+
}
19+
for (col_name, default_expr) in column_defaults {
20+
if !map.contains_key(col_name)
21+
&& let Some(val) = evaluate_default_expr(default_expr)
22+
{
23+
map.insert(col_name.clone(), val);
24+
}
25+
}
26+
nodedb_types::Value::Object(map)
27+
})
28+
.collect();
29+
let val = nodedb_types::Value::Array(arr);
30+
nodedb_types::value_to_msgpack(&val).map_err(|e| crate::Error::Serialization {
31+
format: "msgpack".into(),
32+
detail: format!("columnar row batch: {e}"),
33+
})
34+
}

0 commit comments

Comments
 (0)