@@ -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