Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4b00406
test(sql): cover sequence functions and DEFAULT expressions across en…
farhan-syah Sep 9, 2026
589ce2e
test(sql): cover volatile DEFAULT expression re-evaluation
farhan-syah Sep 9, 2026
aaf4a87
fix(sql): recover the DEFAULT clause on schemaless collections
farhan-syah Sep 9, 2026
f795153
feat(sql): add nextval/currval/setval sequence functions
farhan-syah Sep 9, 2026
33987eb
fix(sql): evaluate sequence-backed column DEFAULTs at plan time
farhan-syah Sep 9, 2026
49cb139
feat(sql): expand SERIAL into a nextval DEFAULT and gate declared DEF…
farhan-syah Sep 9, 2026
6e166ef
test(sql): cover plan-only prepare/describe leaving sequences untouched
farhan-syah Sep 9, 2026
dd79709
test(sql): cover declared column types across DEFAULT and NOT NULL
farhan-syah Sep 9, 2026
95428a4
test(sql): cover DEFAULT evaluation on primary='vector' collections
farhan-syah Sep 9, 2026
e643437
feat(sql): materialize declared DEFAULTs for KV and vector-primary in…
farhan-syah Sep 9, 2026
ce64140
feat(sql): announce RETURNING output columns instead of an empty schema
farhan-syah Sep 9, 2026
960dee0
fix(timeseries): read declared TIMESTAMP columns as epoch microseconds
farhan-syah Sep 9, 2026
99a40d4
refactor(sql): derive RETURNING result fields from OutputSchema
farhan-syah Sep 9, 2026
1386fc3
feat(sql): resolve real column types for joins and grouped timeseries…
farhan-syah Sep 10, 2026
299e698
fix(join): rescale declared-instant columns a join scans locally
farhan-syah Sep 10, 2026
7da56d6
fix(timeseries): render GROUP BY keys with their declared column type
farhan-syah Sep 10, 2026
640cce8
test(timeseries): cover a declared time key rendered via join and GRO…
farhan-syah Sep 10, 2026
7927b16
refactor(sql): move write-route resolution into EngineRules
farhan-syah Sep 10, 2026
4be0b77
fix(join): rescale instant cells before projection renames them
farhan-syah Sep 10, 2026
d530c30
refactor(types): unify declared-type resolution and instant detection
farhan-syah Sep 10, 2026
caa424b
refactor(sql): compile column DEFAULTs once per statement
farhan-syah Sep 10, 2026
3cbbb19
feat(sql): unify declared-type parsing and gate value-producing clauses
farhan-syah Sep 10, 2026
7464d2c
fix(sql): refuse per-row sequence accessors instead of returning NULL
farhan-syah Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use nodedb::control::gateway::core::QueryContext;
use nodedb::control::gateway::plan_cache::PlanCacheKey;
use nodedb::control::gateway::plan_cache::{hash_placeholder_types, hash_sql};
use nodedb::control::gateway::version_set::GatewayVersionSet;
use nodedb::control::gateway::{Gateway, PlanCache};
use nodedb::control::gateway::{Gateway, LoweredPlan, PlanCache};
use nodedb::types::TenantId;
use nodedb_physical::physical_plan::{KvOp, PhysicalPlan};
use nodedb_types::QualifiedCollection;
Expand Down Expand Up @@ -135,15 +135,15 @@ async fn gateway_execute_sql_plan_cache_populated() {

let sql = "GET gw_cache_smoke smoke-key";
let make_plan = || {
Ok(PhysicalPlan::Kv(KvOp::Get {
Ok(LoweredPlan::cacheable(PhysicalPlan::Kv(KvOp::Get {
collection: QualifiedCollection::new(
nodedb_types::id::DatabaseId::DEFAULT,
"gw_cache_smoke",
),
key: b"smoke-key".to_vec(),
rls_filters: vec![],
surrogate_ceiling: None,
}))
})))
};

// Cache starts empty.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ use std::sync::Arc;
use std::time::Duration;

use nodedb::Error;
use nodedb::control::gateway::Gateway;
use nodedb::control::gateway::GatewayErrorMap;
use nodedb::control::gateway::core::QueryContext;
use nodedb::control::gateway::{Gateway, LoweredPlan};
use nodedb::types::TenantId;
use nodedb_physical::physical_plan::{KvOp, PhysicalPlan};
use nodedb_types::QualifiedCollection;
Expand Down Expand Up @@ -184,15 +184,15 @@ async fn http_gateway_migration_cross_node_query() {
get_sql,
&[],
|| {
Ok(PhysicalPlan::Kv(KvOp::Get {
Ok(LoweredPlan::cacheable(PhysicalPlan::Kv(KvOp::Get {
collection: QualifiedCollection::new(
nodedb_types::id::DatabaseId::DEFAULT,
"http_gw_cross_node",
),
key: b"cross-key".to_vec(),
rls_filters: vec![],
surrogate_ceiling: None,
}))
})))
},
|plan| async {
Ok(common::authorize_gateway_plan(&follower.shared, &ctx, plan).await)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use crate::common;
use std::sync::Arc;
use std::time::Duration;

use nodedb::control::gateway::Gateway;
use nodedb::control::gateway::core::QueryContext;
use nodedb::control::gateway::version_set::GatewayVersionSet;
use nodedb::control::gateway::{Gateway, LoweredPlan};
use nodedb::types::TenantId;
use nodedb_physical::physical_plan::{KvOp, PhysicalPlan};
use nodedb_types::QualifiedCollection;
Expand Down Expand Up @@ -200,15 +200,15 @@ async fn pgwire_gateway_migration_plan_cache_hits() {

let sql = "GET pgwire_gw_cache cache-key";
let make_plan = || {
Ok(PhysicalPlan::Kv(KvOp::Get {
Ok(LoweredPlan::cacheable(PhysicalPlan::Kv(KvOp::Get {
collection: QualifiedCollection::new(
nodedb_types::id::DatabaseId::DEFAULT,
"pgwire_gw_cache",
),
key: b"cache-key".to_vec(),
rls_filters: vec![],
surrogate_ceiling: None,
}))
})))
};
let authorize_plan = |plan: PhysicalPlan| async {
Ok(common::authorize_gateway_plan(&node.shared, &ctx, plan).await)
Expand Down
48 changes: 48 additions & 0 deletions nodedb-sql/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,54 @@ pub trait SqlCatalog {
fn resolve_regtype(&self, _name: &str) -> Option<i64> {
None
}

/// Advance a sequence and return the allocated value, recording it as this
/// session's `currval`. Scoping mirrors `resolve_regclass`.
///
/// An unknown name returns [`SqlError::UndefinedObject`] (SQLSTATE
/// `42704`). The default returns "sequence access unavailable" so
/// implementors with no sequence state compile without change.
fn sequence_nextval(
&self,
_database_id: nodedb_types::DatabaseId,
_tenant_id: u64,
_name: &str,
) -> Result<i64, crate::SqlError> {
Err(sequence_access_unavailable())
}

/// Return the last value this session obtained from `sequence_nextval`.
///
/// A sequence this session never advanced returns
/// [`SqlError::ObjectNotInPrerequisiteState`] (SQLSTATE `55000`).
fn sequence_currval(
&self,
_database_id: nodedb_types::DatabaseId,
_tenant_id: u64,
_name: &str,
) -> Result<i64, crate::SqlError> {
Err(sequence_access_unavailable())
}

/// Position a sequence so the next `sequence_nextval` returns
/// `value + increment`. Returns `value`.
fn sequence_setval(
&self,
_database_id: nodedb_types::DatabaseId,
_tenant_id: u64,
_name: &str,
_value: i64,
) -> Result<i64, crate::SqlError> {
Err(sequence_access_unavailable())
}
}

/// The error a catalog with no sequence state returns for every accessor.
fn sequence_access_unavailable() -> crate::SqlError {
crate::SqlError::ObjectNotInPrerequisiteState {
object: "sequence".into(),
detail: "sequence access unavailable: this catalog carries no sequence registry".into(),
}
}

/// View of a registered array, surfaced to the SQL planner. Decoded by
Expand Down
2 changes: 2 additions & 0 deletions nodedb-sql/src/engine_rules/columnar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ impl EngineRules for ColumnarRules {
Ok(vec![SqlPlan::Insert {
collection: p.collection,
engine: EngineType::Columnar,
route: WriteRoute::ColumnarFamily,
rows: p.rows,
column_defaults: p.column_defaults,
if_absent: p.if_absent,
Expand All @@ -29,6 +30,7 @@ impl EngineRules for ColumnarRules {
Ok(vec![SqlPlan::Upsert {
collection: p.collection,
engine: EngineType::Columnar,
route: WriteRoute::ColumnarFamily,
rows: p.rows,
column_defaults: p.column_defaults,
on_conflict_updates: p.on_conflict_updates,
Expand Down
2 changes: 2 additions & 0 deletions nodedb-sql/src/engine_rules/document_schemaless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ impl EngineRules for SchemalessRules {
Ok(vec![SqlPlan::Insert {
collection: p.collection,
engine: EngineType::DocumentSchemaless,
route: WriteRoute::Document,
rows: p.rows,
column_defaults: p.column_defaults,
if_absent: p.if_absent,
Expand All @@ -25,6 +26,7 @@ impl EngineRules for SchemalessRules {
Ok(vec![SqlPlan::Upsert {
collection: p.collection,
engine: EngineType::DocumentSchemaless,
route: WriteRoute::Document,
rows: p.rows,
column_defaults: p.column_defaults,
on_conflict_updates: p.on_conflict_updates,
Expand Down
2 changes: 2 additions & 0 deletions nodedb-sql/src/engine_rules/document_strict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ impl EngineRules for StrictRules {
Ok(vec![SqlPlan::Insert {
collection: p.collection,
engine: EngineType::DocumentStrict,
route: WriteRoute::Document,
rows: p.rows,
column_defaults: p.column_defaults,
if_absent: p.if_absent,
Expand All @@ -25,6 +26,7 @@ impl EngineRules for StrictRules {
Ok(vec![SqlPlan::Upsert {
collection: p.collection,
engine: EngineType::DocumentStrict,
route: WriteRoute::Document,
rows: p.rows,
column_defaults: p.column_defaults,
on_conflict_updates: p.on_conflict_updates,
Expand Down
2 changes: 2 additions & 0 deletions nodedb-sql/src/engine_rules/spatial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ impl EngineRules for SpatialRules {
Ok(vec![SqlPlan::Insert {
collection: p.collection,
engine: EngineType::Spatial,
route: WriteRoute::ColumnarFamily,
rows: p.rows,
column_defaults: p.column_defaults,
if_absent: p.if_absent,
Expand All @@ -28,6 +29,7 @@ impl EngineRules for SpatialRules {
Ok(vec![SqlPlan::Upsert {
collection: p.collection,
engine: EngineType::Spatial,
route: WriteRoute::ColumnarFamily,
rows: p.rows,
column_defaults: p.column_defaults,
on_conflict_updates: p.on_conflict_updates,
Expand Down
48 changes: 48 additions & 0 deletions nodedb-sql/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,35 @@ pub enum SqlError {
#[error("function {name}(...) does not exist")]
UndefinedFunction { name: String },

/// A sequence accessor appeared where every output row needs its own
/// allocation, such as a SELECT list over a FROM clause.
///
/// Rendered as SQLSTATE `0A000` (feature_not_supported). Constant
/// contexts evaluate the call for real: a FROM-less `SELECT`, a column
/// `DEFAULT`, a `VALUES` list. The refusal keeps a per-row call from
/// reaching the row evaluator, which has no sequence state and would
/// return `NULL` for every row.
#[error(
"{name}(...) is not supported in a per-row context; \
a SELECT list, WHERE clause, or SET clause over a FROM relation \
evaluates once per row. Call it in a FROM-less SELECT or a column \
DEFAULT instead"
)]
SequencePerRowUnsupported { name: String },

/// A statement names a database object that does not exist — a sequence,
/// most commonly. Distinct from [`SqlError::UndefinedFunction`]: the
/// function exists, the object it names does not. PostgreSQL rejects the
/// same input with SQLSTATE `42704` (`undefined_object`).
#[error("{kind} \"{name}\" does not exist")]
UndefinedObject { kind: &'static str, name: String },

/// An object exists but a prerequisite step has not run, such as `currval`
/// before this session called `nextval`. PostgreSQL rejects the same input
/// with SQLSTATE `55000` (`object_not_in_prerequisite_state`).
#[error("{detail}")]
ObjectNotInPrerequisiteState { object: String, detail: String },

#[error("unknown column '{column}' in table '{table}'")]
UnknownColumn { table: String, column: String },

Expand Down Expand Up @@ -85,6 +114,25 @@ pub enum SqlError {
#[error("unsupported: {detail}")]
Unsupported { detail: String },

/// A declared column DEFAULT the server cannot evaluate to a value.
///
/// The column is never omitted instead. A DEFAULT that disappears stores
/// NULL where the declaration promised a value, and nothing reports it.
#[error("DEFAULT for column '{column}' cannot be evaluated: {expr}")]
UnevaluableDefault { column: String, expr: String },

/// `setval` appeared inside a column DEFAULT.
///
/// A DEFAULT runs once per row, so evaluating `setval` there will move the
/// sequence's position on every inserted row. PostgreSQL reports a
/// function used in a context that forbids it as SQLSTATE `42601`
/// (`syntax_error`), and this refusal carries the same code.
#[error(
"setval() is not allowed in the DEFAULT for column '{column}'; \
a DEFAULT must not move a sequence's position"
)]
SetvalInColumnDefault { column: String },

#[error("invalid function call: {detail}")]
InvalidFunction { detail: String },

Expand Down
8 changes: 8 additions & 0 deletions nodedb-sql/src/functions/arg_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,11 @@ pub static JSON_CONTAINS_ARGS: &[ArgTypeSpec] = &[any("container"), any("needle"

/// `json_merge(base, overlay)` / `json_patch(base, overlay)`.
pub static JSON_MERGE_ARGS: &[ArgTypeSpec] = &[any("base"), any("overlay")];

// ── Sequence accessors ───────────────────────────────────────────────────────

/// `nextval(name)` / `currval(name)` — one sequence name.
pub static SEQUENCE_NAME_ARGS: &[ArgTypeSpec] = &[typed("sequence", TEXT)];

/// `setval(name, value)` — a sequence name and the value it must take.
pub static SETVAL_ARGS: &[ArgTypeSpec] = &[typed("sequence", TEXT), typed("value", INT64_ONLY)];
1 change: 1 addition & 0 deletions nodedb-sql/src/functions/builtins/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ pub(super) fn m(
return_type,
arg_types,
since: V0_1_0,
volatility: nodedb_types::Volatility::Immutable,
}
}
2 changes: 2 additions & 0 deletions nodedb-sql/src/functions/builtins/scalars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod math;
mod misc;
mod pg_fts;
mod pg_json;
mod sequence_fn;
mod spatial;
mod string;
mod vector;
Expand All @@ -32,6 +33,7 @@ pub(super) fn scalar_functions() -> Vec<FunctionMeta> {
fns.extend(array_fn::array_fn_functions());
fns.extend(array_elem::array_elem_functions());
fns.extend(id_fn::id_fn_functions());
fns.extend(sequence_fn::sequence_fn_functions());
fns.extend(misc::misc_functions());
fns
}
6 changes: 4 additions & 2 deletions nodedb-sql/src/functions/builtins/scalars/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ pub(super) fn datetime_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Timestamptz),
arg_types::NO_ARGS,
),
)
.volatile(),
m(
"current_timestamp",
Scalar,
Expand All @@ -53,7 +54,8 @@ pub(super) fn datetime_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Timestamptz),
arg_types::NO_ARGS,
),
)
.volatile(),
m(
"datetime",
Scalar,
Expand Down
21 changes: 14 additions & 7 deletions nodedb-sql/src/functions/builtins/scalars/id_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Uuid),
arg_types::NO_ARGS,
),
)
.volatile(),
// `uuid_v4` and `gen_random_uuid` are aliases for `uuid` — same
// `nodedb_query::functions::id::try_eval` match arm
// (`"uuid" | "uuid_v4" | "gen_random_uuid"`).
Expand All @@ -43,7 +44,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Uuid),
arg_types::NO_ARGS,
),
)
.volatile(),
m(
"gen_random_uuid",
Scalar,
Expand All @@ -52,7 +54,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Uuid),
arg_types::NO_ARGS,
),
)
.volatile(),
m(
"uuid_v7",
Scalar,
Expand All @@ -61,7 +64,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Uuid),
arg_types::NO_ARGS,
),
)
.volatile(),
m(
"ulid",
Scalar,
Expand All @@ -70,7 +74,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::Ulid),
arg_types::NO_ARGS,
),
)
.volatile(),
m(
"cuid2",
Scalar,
Expand All @@ -79,7 +84,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::String),
arg_types::NO_ARGS,
),
)
.volatile(),
// `nanoid(length?)` — the optional length argument means max_args is
// 1 even though the common call form `nanoid()` takes none.
m(
Expand All @@ -90,7 +96,8 @@ pub(super) fn id_fn_functions() -> Vec<FunctionMeta> {
no_trigger(),
Some(ColumnType::String),
arg_types::NANOID_ARGS,
),
)
.volatile(),
m(
"is_uuid",
Scalar,
Expand Down
Loading
Loading