risingwavelabs/risingwave · error
`LogicalNow` can only be converted to stream
Error message
`LogicalNow` can only be converted to stream
What it means
LogicalNow represents the non-deterministic now() function. In RisingWave, now() is only meaningful in streaming plans (where it is materialized as a streaming expression with provisional/refresh semantics); there is no batch lowering, so to_batch explicitly errors. For batch queries users should use CURRENT_TIMESTAMP (fixed at query start) instead.
Solutions
- Use CURRENT_TIMESTAMP instead of now() in batch queries
- In INSERT ... SELECT batch statements, replace now() with CURRENT_TIMESTAMP
- Check frontend rewrite rules if you maintain the codebase: ensure now() in batch contexts is rewritten to CurrentTimestamp before ToBatch
- If it appears in a materialized view path that unexpectedly goes batch, verify the query is not being forced into batch mode
Example fix
-- before (fails in batch) SELECT now(); INSERT INTO t SELECT now(), ...; -- after SELECT CURRENT_TIMESTAMP; INSERT INTO t SELECT CURRENT_TIMESTAMP, ...;
Defensive patterns
Strategy: validation
Validate before calling
-- In batch queries, always use CURRENT_TIMESTAMP instead of now()
-- static check in app code:
-- if /\bnow\s*\(\s*\)/i.test(batchSql) throw new Error('use CURRENT_TIMESTAMP in batch queries'); Try / catch
try {
await conn.query('SELECT now()');
} catch (e) {
if (String(e.message).includes('`LogicalNow` can only be converted to stream')) {
await conn.query('SELECT CURRENT_TIMESTAMP');
} else { throw e; }
} Prevention
- Use now() only inside materialized views / streaming queries
- Use CURRENT_TIMESTAMP for batch and INSERT ... SELECT statements
- Lint SQL for now() in batch execution paths
When it happens
Trigger: Calling to_batch on a plan containing LogicalNow — typically using now() inside a batch SELECT, batch DML, or a query path that is forced through batch optimization (e.g. inside a batch exec of a DML statement where now() was not rewritten).
Common situations: Users write INSERT ... SELECT now() or batch SELECT now() expecting wall-clock time; frontend rewrites usually substitute CURRENT_TIMESTAMP for now() in batch contexts, and this error surfaces when a now() survives into batch optimization.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- BatchGapFill is not implemented yet
- Conditions containing now must be in the form of…
- Expr error
- no affected rows in output
- query_epoch not set in distributed lookup join
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/e316502c8ab805f1.
Report an issue: GitHub.
Appendix: source
Thrown at src/frontend/src/optimizer/plan_node/logical_now.rs:98
fn logical_rewrite_for_stream(
&self,
_ctx: &mut RewriteStreamContext,
) -> Result<(PlanRef, ColIndexMapping)> {
Ok((self.clone().into(), ColIndexMapping::new(vec![Some(0)], 1)))
}
/// `to_stream` is equivalent to `to_stream_with_dist_required(RequiredDist::Any)`
fn to_stream(
&self,
_ctx: &mut ToStreamContext,
) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
Ok(StreamNow::new(self.core.clone()).into())
}
}
impl ToBatch for LogicalNow {
fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {
bail!("`LogicalNow` can only be converted to stream")
}
}
/// The trait for column pruning, only logical plan node will use it, though all plan node impl it.
impl ColPrunable for LogicalNow {
fn prune_col(&self, required_cols: &[usize], _: &mut ColumnPruningContext) -> PlanRef {
if required_cols.is_empty() {
LogicalValues::new(vec![], Schema::empty().clone(), self.ctx()).into()
} else {
assert_eq!(required_cols, &[0], "we only output one column");
self.clone().into()
}
}
}
View on GitHub (pinned to 6469eb736d)