Skip to content

Commit 19c3929

Browse files
committed
fix(pgwire): evaluate scalar functions in DDL value parser
The `parse_inline_value` helper in the pgwire DDL path was converting any unrecognised token directly to a string, causing `now()` and `current_timestamp` in UPSERT payloads to be stored as their literal text rather than the current timestamp. Introduce `try_eval_scalar_function` which routes bare SQL keywords (`current_timestamp`, `current_date`, etc.) and parenthesised call expressions through the same `nodedb_sql::planner::const_fold` evaluator used by the INSERT/UPSERT VALUES planner path. Unknown names fall through to the existing string behaviour.
1 parent e273e1c commit 19c3929

File tree

1 file changed

+73
-0
lines changed

1 file changed

+73
-0
lines changed

nodedb/src/control/server/pgwire/ddl/sql_parse.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,82 @@ pub(super) fn parse_sql_value(val: &str) -> nodedb_types::Value {
6767
if let Ok(f) = trimmed.parse::<f64>() {
6868
return nodedb_types::Value::Float(f);
6969
}
70+
// Scalar function call like `now()` or `date_add(now(), '1h')`, or a
71+
// bare identifier like `current_timestamp` that SQL treats as a
72+
// zero-arg function. Route through the shared evaluator so the
73+
// UPSERT fast-path stays aligned with the SQL planner's VALUES path.
74+
// Unknown names fall through to the legacy string behavior.
75+
if let Some(v) = try_eval_scalar_function(trimmed) {
76+
return v;
77+
}
7078
nodedb_types::Value::String(trimmed.to_string())
7179
}
7280

81+
/// Evaluate a scalar function expression like `now()` or a bare SQL
82+
/// keyword like `current_timestamp` via the shared `nodedb_query`
83+
/// evaluator. Returns `None` if the input isn't a recognizable call
84+
/// form or the function is unknown.
85+
fn try_eval_scalar_function(s: &str) -> Option<nodedb_types::Value> {
86+
// Bare identifier: SQL treats `current_timestamp`, `current_date`,
87+
// etc. as zero-arg function references without parentheses.
88+
let is_bare_ident = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
89+
&& !s.is_empty()
90+
&& !s.chars().next().is_some_and(|c| c.is_ascii_digit());
91+
92+
if is_bare_ident {
93+
let name = s.to_lowercase();
94+
// Only fold if the registry knows this name. Gate via nodedb-sql's
95+
// registry so we don't accidentally evaluate user identifiers.
96+
let registry = nodedb_sql::planner::const_fold::default_registry();
97+
if registry.lookup(&name).is_some() {
98+
let val = nodedb_query::functions::eval_function(&name, &[]);
99+
if !matches!(val, nodedb_types::Value::Null) {
100+
return Some(val);
101+
}
102+
}
103+
return None;
104+
}
105+
106+
// Call form `name(args...)`. Parse via sqlparser + fold via const_fold.
107+
if !s.ends_with(')') || !s.contains('(') {
108+
return None;
109+
}
110+
let stmt_sql = format!("SELECT {s}");
111+
let dialect = sqlparser::dialect::PostgreSqlDialect {};
112+
let stmts = sqlparser::parser::Parser::parse_sql(&dialect, &stmt_sql).ok()?;
113+
let stmt = stmts.into_iter().next()?;
114+
let sqlparser::ast::Statement::Query(query) = stmt else {
115+
return None;
116+
};
117+
let sqlparser::ast::SetExpr::Select(select) = *query.body else {
118+
return None;
119+
};
120+
let item = select.projection.into_iter().next()?;
121+
let ast_expr = match item {
122+
sqlparser::ast::SelectItem::UnnamedExpr(e)
123+
| sqlparser::ast::SelectItem::ExprWithAlias { expr: e, .. } => e,
124+
_ => return None,
125+
};
126+
let sql_expr = nodedb_sql::resolver::expr::convert_expr(&ast_expr).ok()?;
127+
let folded = nodedb_sql::planner::const_fold::fold_constant_default(&sql_expr)?;
128+
Some(sql_value_to_ndb_value(folded))
129+
}
130+
131+
fn sql_value_to_ndb_value(v: nodedb_sql::types::SqlValue) -> nodedb_types::Value {
132+
use nodedb_sql::types::SqlValue;
133+
match v {
134+
SqlValue::Null => nodedb_types::Value::Null,
135+
SqlValue::Bool(b) => nodedb_types::Value::Bool(b),
136+
SqlValue::Int(i) => nodedb_types::Value::Integer(i),
137+
SqlValue::Float(f) => nodedb_types::Value::Float(f),
138+
SqlValue::String(s) => nodedb_types::Value::String(s),
139+
SqlValue::Bytes(b) => nodedb_types::Value::Bytes(b),
140+
SqlValue::Array(a) => {
141+
nodedb_types::Value::Array(a.into_iter().map(sql_value_to_ndb_value).collect())
142+
}
143+
}
144+
}
145+
73146
/// Extract a clause value delimited by known keywords.
74147
///
75148
/// Given `upper = "TYPE INT DEFAULT 0 ASSERT $value > 0"`, `original` (same

0 commit comments

Comments
 (0)