From 304df7eaa8b52a9edb66a6829a544e2115ee9a7f Mon Sep 17 00:00:00 2001 From: 81reap Date: Wed, 11 Nov 2026 23:25:58 -0500 Subject: [PATCH 1/2] refactor(database) :: decode rows into named columns first Behavior does not change. Refs #1056 --- src/webserver/database/execute_queries.rs | 6 ++-- src/webserver/database/sql_to_json.rs | 34 ++++++++++++----------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/webserver/database/execute_queries.rs b/src/webserver/database/execute_queries.rs index 475380d44..8a9478308 100644 --- a/src/webserver/database/execute_queries.rs +++ b/src/webserver/database/execute_queries.rs @@ -643,8 +643,10 @@ fn parse_single_sql_result( if log::log_enabled!(log::Level::Trace) { debug_row(&r); } - let (row, mut inputs) = - super::sql_to_json::row_to_json_with_inputs(&r, query.row_input_json.len())?; + let (row, mut inputs) = super::sql_to_json::row_to_json_with_inputs( + super::sql_to_json::decode_columns(&r), + query.row_input_json.len(), + )?; decode_json_values(&mut inputs, &query.row_input_json); Ok(QueryEvent::Row(QueryRow { value: row, diff --git a/src/webserver/database/sql_to_json.rs b/src/webserver/database/sql_to_json.rs index 359471d2a..7fec8d071 100644 --- a/src/webserver/database/sql_to_json.rs +++ b/src/webserver/database/sql_to_json.rs @@ -44,29 +44,31 @@ pub(super) fn row_to_json(row: &AnyRow) -> Value { Object(map) } -/// Decodes a row into user-visible columns and a private trailing input suffix. +pub(super) fn decode_columns(row: &AnyRow) -> Vec<(String, Value)> { + row.columns() + .iter() + .map(|column| (canonical_col_name(column), sql_to_json(row, column))) + .collect() +} + +/// Splits decoded columns into user-visible columns and a private trailing input suffix. /// -/// Every SQL value is decoded exactly once. Private values are addressed by -/// ordinal, so their generated SQL aliases cannot collide with user columns. +/// Private values are addressed by ordinal, so their generated SQL aliases +/// cannot collide with user columns. pub(super) fn row_to_json_with_inputs( - row: &AnyRow, + mut columns: Vec<(String, Value)>, input_count: usize, ) -> anyhow::Result<(Value, Vec)> { - let columns = row.columns(); let public_count = columns .len() .checked_sub(input_count) .ok_or_else(|| anyhow::anyhow!("The query returned fewer columns than SQLPage expected"))?; - let mut map = Map::new(); - let mut inputs = Vec::with_capacity(input_count); - for column in &columns[..public_count] { - let key = canonical_col_name(column); - let value = sql_to_json(row, column); - map = add_value_to_map(map, (key, value)); - } - for column in &columns[public_count..] { - inputs.push(sql_to_json(row, column)); - } + let inputs = columns + .split_off(public_count) + .into_iter() + .map(|(_, value)| value) + .collect(); + let map = columns.into_iter().fold(Map::new(), add_value_to_map); Ok((Value::Object(map), inputs)) } @@ -263,7 +265,7 @@ mod tests { ) .fetch_one(&mut connection) .await?; - let (public, inputs) = row_to_json_with_inputs(&row, 1)?; + let (public, inputs) = row_to_json_with_inputs(decode_columns(&row), 1)?; expect_json_object_equal( &public, &serde_json::json!({ "__sqlpage_input_0": "public" }), From 54a232039d981fa81815e8fc56b82eef44f9fb2a Mon Sep 17 00:00:00 2001 From: lovasoa Date: Thu, 12 Nov 2026 23:45:50 +0100 Subject: [PATCH 2/2] avoid unneeded allocation in row_to_json_with_inputs --- src/webserver/database/execute_queries.rs | 6 ++-- src/webserver/database/sql_to_json.rs | 37 ++++++++++++----------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/webserver/database/execute_queries.rs b/src/webserver/database/execute_queries.rs index 8a9478308..475380d44 100644 --- a/src/webserver/database/execute_queries.rs +++ b/src/webserver/database/execute_queries.rs @@ -643,10 +643,8 @@ fn parse_single_sql_result( if log::log_enabled!(log::Level::Trace) { debug_row(&r); } - let (row, mut inputs) = super::sql_to_json::row_to_json_with_inputs( - super::sql_to_json::decode_columns(&r), - query.row_input_json.len(), - )?; + let (row, mut inputs) = + super::sql_to_json::row_to_json_with_inputs(&r, query.row_input_json.len())?; decode_json_values(&mut inputs, &query.row_input_json); Ok(QueryEvent::Row(QueryRow { value: row, diff --git a/src/webserver/database/sql_to_json.rs b/src/webserver/database/sql_to_json.rs index 7fec8d071..386c833bd 100644 --- a/src/webserver/database/sql_to_json.rs +++ b/src/webserver/database/sql_to_json.rs @@ -44,31 +44,32 @@ pub(super) fn row_to_json(row: &AnyRow) -> Value { Object(map) } -pub(super) fn decode_columns(row: &AnyRow) -> Vec<(String, Value)> { +/// Decodes a database row into (key, value) pairs +pub(super) fn decode_columns(row: &AnyRow) -> impl ExactSizeIterator { row.columns() .iter() .map(|column| (canonical_col_name(column), sql_to_json(row, column))) - .collect() } -/// Splits decoded columns into user-visible columns and a private trailing input suffix. +/// A row returned by the database starts with a certain number of user-accessible "public" values, +/// followed by values generated internally by sqlpage's query rewriting engine to serve as inputs to +/// sqlpage functions. /// -/// Private values are addressed by ordinal, so their generated SQL aliases -/// cannot collide with user columns. +/// Given `[(public_key_1, public_val_1), (public_key_2, public_val_2), (internal_key_1, internal_val_1)], 1` +/// This function returns `{public_key_1:public_val_1, public_key_2:public_val_2}, [internal_val_1]` +/// +/// If there are fewer than `internal_count` columns, all columns are considered internal pub(super) fn row_to_json_with_inputs( - mut columns: Vec<(String, Value)>, - input_count: usize, + row: &AnyRow, + internal_count: usize, ) -> anyhow::Result<(Value, Vec)> { - let public_count = columns - .len() - .checked_sub(input_count) - .ok_or_else(|| anyhow::anyhow!("The query returned fewer columns than SQLPage expected"))?; - let inputs = columns - .split_off(public_count) - .into_iter() - .map(|(_, value)| value) - .collect(); - let map = columns.into_iter().fold(Map::new(), add_value_to_map); + let mut db_pairs = decode_columns(row); + let mut map = Map::new(); + while db_pairs.len() > internal_count { + map = add_value_to_map(map, db_pairs.next().expect("len was checked")); + } + anyhow::ensure!(db_pairs.len() == internal_count, "missing internal columns"); + let inputs = db_pairs.map(|(_, value)| value).collect(); Ok((Value::Object(map), inputs)) } @@ -265,7 +266,7 @@ mod tests { ) .fetch_one(&mut connection) .await?; - let (public, inputs) = row_to_json_with_inputs(decode_columns(&row), 1)?; + let (public, inputs) = row_to_json_with_inputs(&row, 1)?; expect_json_object_equal( &public, &serde_json::json!({ "__sqlpage_input_0": "public" }),