Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 20 additions & 11 deletions nodedb/src/control/planner/sql_plan_convert/output_schema/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,26 @@ pub fn build_output_schema<C: SqlCatalog + ?Sized>(
let types = super::join_types::join_column_types(left, right, catalog, database_id);
schema_from_projection(projection, &types, &[])
}
SqlPlan::ConstantResult { columns, .. } => OutputSchema {
columns: columns
.iter()
.map(|c| OutputColumn {
display_name: c.clone(),
lookup_key: c.clone(),
ty: DdlColType::Text,
})
.collect(),
is_star: false,
},
SqlPlan::ConstantResult { columns, .. } => {
// The row payload keys each cell by the unique per-column key
// (`cell_keys`), not the raw display name: two constant columns may
// share a name (`SELECT nextval('s'), nextval('s')`), and a single
// JSON object would collapse them. `display_name` keeps the
// client-facing name; `lookup_key` is the cell key.
let lookup_keys = crate::control::server::response_shape::project::cell_keys(columns);
OutputSchema {
columns: columns
.iter()
.zip(lookup_keys)
.map(|(c, lookup_key)| OutputColumn {
display_name: c.clone(),
lookup_key,
ty: DdlColType::Text,
})
.collect(),
is_star: false,
}
}
SqlPlan::Aggregate {
input,
group_by,
Expand Down
10 changes: 8 additions & 2 deletions nodedb/src/control/planner/sql_plan_convert/set_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ pub(super) fn convert_constant_result(
tenant_id: TenantId,
ctx: &ConvertContext,
) -> crate::Result<Vec<PhysicalTask>> {
// A constant row is one JSON object, which cannot hold two cells under one
// key. `SELECT nextval('s'), nextval('s')` legally repeats an output name;
// keying both cells by the name would collapse them to the last value. Use
// the same unique per-column keys every response encoder derives, so each
// column keeps its own cell.
let cell_keys = crate::control::server::response_shape::project::cell_keys(columns);
let mut obj = serde_json::Map::new();
for (col, val) in columns.iter().zip(values.iter()) {
for ((_col, val), key) in columns.iter().zip(values.iter()).zip(cell_keys.iter()) {
let json_val = match val {
SqlValue::Null => serde_json::Value::Null,
other => serde_json::Value::String(sql_value_to_string(other)),
};
obj.insert(col.clone(), json_val);
obj.insert(key.clone(), json_val);
}
let arr = serde_json::Value::Array(vec![serde_json::Value::Object(obj)]);
let payload = nodedb_types::json_to_msgpack(&arr).map_err(|e| crate::Error::Serialization {
Expand Down
31 changes: 31 additions & 0 deletions nodedb/tests/wire/cases/sql_sequences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,34 @@ async fn a_refused_accessor_produces_no_row_and_no_allocation() {
"the refused statement must not have allocated, got {first:?}"
);
}

/// A from-less projection legally repeats an output name
/// (`SELECT nextval('s'), nextval('s')`). The constant row is a single JSON
/// object, so keying both cells by the display name collapsed them to the
/// last value and both wire columns rendered it. Each column must keep its
/// own cell.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn duplicate_constant_column_names_keep_their_own_cells() {
let server = TestServer::start().await;
server
.exec("CREATE SEQUENCE dup_cell_seq START 1 INCREMENT 1")
.await
.unwrap();

let rows = server
.query_rows("SELECT nextval('dup_cell_seq'), nextval('dup_cell_seq')")
.await
.expect("a from-less projection repeating an output name must return one row");

assert_eq!(
rows.len(),
1,
"expected exactly one constant row, got {rows:?}"
);
assert_eq!(
rows[0],
vec!["1".to_string(), "2".to_string()],
"each duplicate column must keep its own cell; keying both by the \
display name collapses them to the last value"
);
}
Loading