Skip to content

Commit 1743a88

Browse files
usman.yasinUsmanYasin
authored andcommitted
fix: Deterministic column sequence in output
1 parent f4b51b9 commit 1743a88

File tree

5 files changed

+29
-18
lines changed

5 files changed

+29
-18
lines changed

packages/cubejs-backend-native/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/cubeorchestrator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ serde = { version = "1.0.217", features = ["derive"] }
1010
serde_json = "1.0.133"
1111
anyhow = "1.0"
1212
itertools = "0.13.0"
13+
indexmap = { version = "2.0", features = ["serde"] }
1314

1415
[dependencies.neon]
1516
version = "=1"

rust/cubeorchestrator/src/query_message_parser.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{
33
transport::JsRawData,
44
};
55
use cubeshared::codegen::{root_as_http_message, HttpCommand};
6+
use indexmap::IndexMap;
67
use neon::prelude::Finalize;
78
use std::collections::HashMap;
89

@@ -35,7 +36,7 @@ impl std::error::Error for ParseError {}
3536
pub struct QueryResult {
3637
pub columns: Vec<String>,
3738
pub rows: Vec<Vec<DBResponseValue>>,
38-
pub columns_pos: HashMap<String, usize>,
39+
pub columns_pos: IndexMap<String, usize>,
3940
}
4041

4142
impl Finalize for QueryResult {}
@@ -45,7 +46,7 @@ impl QueryResult {
4546
let mut result = QueryResult {
4647
columns: vec![],
4748
rows: vec![],
48-
columns_pos: HashMap::new(),
49+
columns_pos: IndexMap::new(),
4950
};
5051

5152
let http_message =
@@ -69,7 +70,7 @@ impl QueryResult {
6970
return Err(ParseError::ColumnNameNotDefined);
7071
}
7172

72-
let (columns, columns_pos): (Vec<_>, HashMap<_, _>) = result_set_columns
73+
let (columns, columns_pos): (Vec<_>, IndexMap<_, _>) = result_set_columns
7374
.iter()
7475
.enumerate()
7576
.map(|(index, column_name)| {
@@ -111,13 +112,13 @@ impl QueryResult {
111112
return Ok(QueryResult {
112113
columns: vec![],
113114
rows: vec![],
114-
columns_pos: HashMap::new(),
115+
columns_pos: IndexMap::new(),
115116
});
116117
}
117118

118119
let first_row = &js_raw_data[0];
119120
let columns: Vec<String> = first_row.keys().cloned().collect();
120-
let columns_pos: HashMap<String, usize> = columns
121+
let columns_pos: IndexMap<String, usize> = columns
121122
.iter()
122123
.enumerate()
123124
.map(|(index, column)| (column.clone(), index))

rust/cubeorchestrator/src/query_result_transform.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
};
88
use anyhow::{bail, Context, Result};
99
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
10+
use indexmap::IndexMap;
1011
use itertools::multizip;
1112
use serde::{Deserialize, Serialize};
1213
use serde_json::Value;
@@ -171,7 +172,12 @@ pub fn get_members(
171172
alias_to_member_name_map: &HashMap<String, String>,
172173
annotation: &HashMap<String, ConfigItem>,
173174
) -> Result<(MembersMap, Vec<String>)> {
174-
let mut members_map: MembersMap = HashMap::new();
175+
let mut members_map: MembersMap = IndexMap::new();
176+
// IndexMap maintains insertion order, ensuring deterministic column ordering.
177+
// The order comes from db_data.columns which now preserves the database result order
178+
// (since JsRawData uses IndexMap instead of HashMap).
179+
// Not sure if it solves the original comment below.
180+
// Original Comment:
175181
// Hashmaps don't guarantee the order of the elements while iterating
176182
// this fires in get_compact_row because members map doesn't hold the members for
177183
// date range queries, which are added later and thus columns in final recordset are not
@@ -267,13 +273,13 @@ pub fn get_members(
267273

268274
/// Convert DB response object to the compact output format.
269275
pub fn get_compact_row(
270-
members_to_alias_map: &HashMap<String, String>,
276+
members_to_alias_map: &IndexMap<String, String>,
271277
annotation: &HashMap<String, ConfigItem>,
272278
query_type: &QueryType,
273279
members: &[String],
274280
time_dimensions: Option<&Vec<QueryTimeDimension>>,
275281
db_row: &[DBResponseValue],
276-
columns_pos: &HashMap<String, usize>,
282+
columns_pos: &IndexMap<String, usize>,
277283
) -> Result<Vec<DBResponsePrimitive>> {
278284
let mut row: Vec<DBResponsePrimitive> = Vec::with_capacity(members.len());
279285

@@ -322,9 +328,9 @@ pub fn get_vanilla_row(
322328
query_type: &QueryType,
323329
query: &NormalizedQuery,
324330
db_row: &[DBResponseValue],
325-
columns_pos: &HashMap<String, usize>,
326-
) -> Result<HashMap<String, DBResponsePrimitive>> {
327-
let mut row = HashMap::new();
331+
columns_pos: &IndexMap<String, usize>,
332+
) -> Result<IndexMap<String, DBResponsePrimitive>> {
333+
let mut row = IndexMap::new();
328334

329335
// FIXME: For now custom granularities are not supported, only common ones.
330336
// There is no granularity type/class implementation in rust yet.
@@ -527,7 +533,7 @@ pub enum TransformedData {
527533
members: Vec<String>,
528534
dataset: Vec<Vec<DBResponsePrimitive>>,
529535
},
530-
Vanilla(Vec<HashMap<String, DBResponsePrimitive>>),
536+
Vanilla(Vec<IndexMap<String, DBResponsePrimitive>>),
531537
}
532538

533539
impl TransformedData {
@@ -2209,7 +2215,7 @@ mod tests {
22092215
alias_to_member_name_map,
22102216
annotation,
22112217
)?;
2212-
let members_map_expected: MembersMap = HashMap::from([
2218+
let members_map_expected: MembersMap = IndexMap::from([
22132219
(
22142220
"ECommerceRecordsUs2021.postalCode".to_string(),
22152221
"e_commerce_records_us2021__postal_code".to_string(),
@@ -2270,7 +2276,7 @@ mod tests {
22702276
alias_to_member_name_map,
22712277
annotation,
22722278
)?;
2273-
let members_map_expected: MembersMap = HashMap::from([
2279+
let members_map_expected: MembersMap = IndexMap::from([
22742280
(
22752281
"ECommerceRecordsUs2021.orderDate.day".to_string(),
22762282
"e_commerce_records_us2021__order_date_day".to_string(),
@@ -2345,7 +2351,7 @@ mod tests {
23452351
alias_to_member_name_map,
23462352
annotation,
23472353
)?;
2348-
let members_map_expected: HashMap<String, String> = HashMap::from([
2354+
let members_map_expected: MembersMap = IndexMap::from([
23492355
(
23502356
"ECommerceRecordsUs2021.orderDate.month".to_string(),
23512357
"e_commerce_records_us2021__order_date_month".to_string(),
@@ -2640,7 +2646,7 @@ mod tests {
26402646
&raw_data.rows[0],
26412647
&raw_data.columns_pos,
26422648
)?;
2643-
let expected = HashMap::from([
2649+
let expected = IndexMap::from([
26442650
(
26452651
"ECommerceRecordsUs2021.city".to_string(),
26462652
DBResponsePrimitive::String("Missouri City".to_string()),

rust/cubeorchestrator/src/transport.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::query_result_transform::DBResponsePrimitive;
2+
use indexmap::IndexMap;
23
use serde::{Deserialize, Serialize};
34
use serde_json::Value;
45
use std::{collections::HashMap, fmt::Display};
@@ -153,7 +154,7 @@ pub struct QueryTimeDimension {
153154

154155
pub type AliasToMemberMap = HashMap<String, String>;
155156

156-
pub type MembersMap = HashMap<String, String>;
157+
pub type MembersMap = IndexMap<String, String>;
157158

158159
#[derive(Debug, Clone, Serialize, Deserialize)]
159160
pub struct GranularityMeta {
@@ -317,4 +318,4 @@ pub struct TransformDataRequest {
317318
pub res_type: Option<ResultType>,
318319
}
319320

320-
pub type JsRawData = Vec<HashMap<String, DBResponsePrimitive>>;
321+
pub type JsRawData = Vec<IndexMap<String, DBResponsePrimitive>>;

0 commit comments

Comments
 (0)