Skip to content

fix(sql): evaluate now() / current_timestamp at plan time (#33)#35

Merged
farhan-syah merged 3 commits intomainfrom
fix/now-function-evaluation
Apr 15, 2026
Merged

fix(sql): evaluate now() / current_timestamp at plan time (#33)#35
farhan-syah merged 3 commits intomainfrom
fix/now-function-evaluation

Conversation

@farhan-syah
Copy link
Copy Markdown
Contributor

Summary

Fixes #33: now() / current_timestamp in INSERT/UPSERT VALUES were stored as the literal string "now()", surfacing as 1970-01-01T00:00:00 (epoch 0) when the target column was TIMESTAMP in a STRICT collection — and as the literal string in schemaless / KV / columnar engines. Bare SELECT now() also returned null. The bug was three independent code paths (planner SELECT projection, planner DML VALUES, pgwire UPSERT fast-path) each silently failing to dispatch zero-arg scalar functions to the existing nodedb_query::functions::eval_function evaluator.

Fix

A single shared constant-folding evaluator routes all three paths through the same scalar-function dispatcher. Postgres-compatible semantics: now() snapshots once per statement (matches CURRENT_TIMESTAMP in the SQL standard).

  • nodedb-sql/src/planner/const_fold.rs (new): fold_constant(expr, registry) recursively folds literals, unary/binary arithmetic, and registry-gated scalar function calls via nodedb_query::functions::eval_function. Aggregates/window functions rejected; unknown functions return None so callers keep their fallbacks. Plus default_registry() / fold_constant_default() for call sites that don't thread a registry. nodedb-query added as dep of nodedb-sql.
  • nodedb-sql/src/planner/select.rs: eval_constant_expr collapsed to a one-line delegate. Fixes bare SELECT now().
  • nodedb-sql/src/planner/dml.rs: Function fallthrough in expr_to_sql_value now folds via convert_expr + fold_constant_default before the legacy string fallback. Fixes the planner VALUES path for STRICT, schemaless, KV, and COLUMNAR engines.
  • nodedb/src/control/server/pgwire/ddl/sql_parse.rs: parse_sql_value (the UPSERT fast-path's hand-rolled tokenizer that bypasses sqlparser entirely) now detects bare SQL identifiers like current_timestamp via registry lookup and name(args) forms by re-parsing through sqlparser + folding. Fixes the UPSERT path.

Cluster harness fix (in scope: would otherwise be hidden by the new test coverage)

Adding new integration tests surfaced two latent flakes in the multi-node test harness:

  • .config/nextest.toml: pgwire_gateway_migration was missing from the cluster test group despite spawning a 3-node cluster, so it ran in parallel with the unit-test pool and missed its 10s SWIM convergence deadline. Added.
  • nodedb/tests/common/cluster_harness/{cluster.rs,node.rs}: spawn_three() previously returned as soon as topology_size == 3 and rolling-upgrade compat exit converged, but said nothing about Raft election state. Under host load (e.g. when one cluster test exits and the unit-test pool ramps back up), the first acquire/propose could race the initial heartbeat window and surface as raft error: not leader (leader hint: None). Now waits for every node to report the same non-zero metadata-group leader id via cluster_observer.group_status — symmetric to the existing rolling-upgrade-compat wait. (Earlier draft used the unset raft_status_fn stub on SharedState, which always returned None and broke every cluster test; corrected before commit.)

Test plan

  • Repro added to scripts/test.sql covering STRICT, schemaless, KV, COLUMNAR, and bare SELECT — all show real 2026 timestamps post-fix (was epoch 0 / literal "now()")
  • 5 unit tests in const_fold.rs (now/current_timestamp/literal arithmetic/unknown function/column ref)
  • cargo nextest run --no-fail-fast: 4939/4939 passed, 0 failed, 3 known-flaky cluster retries within nextest's retry budget (down from 8 flakies + 1 hard fail before the harness fix)
  • cargo clippy --all-targets -- -D warnings: clean
  • cargo fmt --all --check: clean

…n time

Introduce `nodedb-sql/src/planner/const_fold.rs` — a plan-time constant
folder that evaluates literal expressions and registered zero-arg scalar
functions (`now()`, `current_timestamp`, `date_add(now(), '1h')`, etc.)
by dispatching through the shared `nodedb_query::functions::eval_function`
evaluator.

Wire the folder into the `INSERT`/`UPSERT` VALUES path (`dml.rs`) and
the SELECT projection path (`select.rs`), replacing the inline duplicate
`eval_constant_expr` logic in both. All three paths now share a single
evaluator, closing the drift that caused `now()` in a VALUES clause to
store the literal string `"now()"` instead of the current timestamp.

Semantics follow the Postgres contract: `now()` and `current_timestamp`
resolve once per statement. Folding at plan time satisfies this and
eliminates per-row runtime dispatch overhead. Functions absent from the
`FunctionRegistry` fall back to the existing string passthrough so no
other callers are affected.
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.
Add `NodeHandle::metadata_group_leader()` which reads the observed
metadata-group leader id from the node's local Raft state, returning
`0` while an election is still in progress.

Wire a leader-stability barrier into `ClusterHarness::spawn_three()`
that polls until every node reports the same non-zero leader id before
returning control to the test. This closes the race where CPU pressure
delayed the initial Raft heartbeats past topology convergence,
producing `not leader (leader hint: None)` errors on the first DDL or
descriptor-lease call issued by the test.

Also add the `pgwire_gateway_migration` binary to the nextest cluster
test group so it runs serialised with the other 3-node integration
tests and does not compete for threads.
@farhan-syah farhan-syah merged commit bc78809 into main Apr 15, 2026
2 checks passed
@farhan-syah farhan-syah deleted the fix/now-function-evaluation branch April 15, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

now() in INSERT/UPSERT VALUES stores literal string "now()" instead of evaluating — TIMESTAMP columns end up as epoch 0

1 participant