use super::super::result::{cql_type_to_data_type, ColumnInfo, QueryRow};
use crate::{
parser::complex_types::ComplexTypeParser,
schema::CqlType,
types::{RowKey, ScanRow, Value},
TableId,
};
use std::collections::HashMap;
use std::sync::Arc;
pub(super) fn parse_table_id(table_id: &TableId) -> (Option<String>, String) {
let table_str = table_id.name();
match table_str.rfind('.') {
Some(dot) => (
Some(table_str[..dot].to_string()),
table_str[dot + 1..].to_string(),
),
None => (None, table_str.to_string()),
}
}
pub(super) fn parse_cql_type_str(type_str: &str) -> Option<CqlType> {
let parser = ComplexTypeParser::new();
parser
.parse_type(type_str)
.ok()
.map(|parsed| parsed.cql_type)
}
pub(super) fn column_info_from_type_str(
name: String,
type_str: &str,
position: usize,
table_name: Option<String>,
) -> ColumnInfo {
let cql_type_opt = parse_cql_type_str(type_str);
let data_type = cql_type_opt
.as_ref()
.map(cql_type_to_data_type)
.unwrap_or(crate::types::DataType::Text);
let mut col_info = ColumnInfo {
name,
data_type,
nullable: true,
position,
table_name,
cql_type: None,
};
if let Some(cql_type) = cql_type_opt {
col_info = col_info.with_cql_type(cql_type);
}
col_info
}
pub fn build_row_from_scan(
key: RowKey,
row: ScanRow,
projection: &[String],
schema: Option<&crate::schema::TableSchema>,
) -> Option<QueryRow> {
let cells = row.into_cells()?;
let pk_hint = schema.map(|s| s.partition_keys.len()).unwrap_or(0);
let mut row_values: HashMap<Arc<str>, Value> = HashMap::with_capacity(cells.len() + pk_hint);
let project = |name: &str| projection.is_empty() || projection.iter().any(|p| p == name);
for (name, col_value) in cells {
if project(&name) {
row_values.insert(name, col_value);
}
}
if let Some(schema) = schema {
match crate::storage::partition_key_codec::decode_partition_key_columns(&key.0, schema) {
Ok(pk_columns) => {
for (name, value) in pk_columns {
if project(&name) {
row_values.insert(name.into(), value);
}
}
}
Err(e) => {
log::warn!(
"Failed to reconstruct partition-key columns from row key \
(len={} bytes) for {}.{}: {}",
key.0.len(),
schema.keyspace,
schema.table,
e
);
}
}
}
Some(QueryRow {
values: row_values,
key,
metadata: Default::default(),
cell_metadata: None,
})
}
#[cfg(test)]
mod tests {
use super::super::predicate::evaluate_predicates;
use super::super::test_support::single_pk_schema;
use super::*;
#[test]
fn build_row_from_scan_materialises_single_text_pk() {
let key = RowKey::new(b"k0000000000000000".to_vec());
let value = ScanRow::Row(vec![(Arc::from("name"), Value::Text("name-0".to_string()))]);
let schema = single_pk_schema("id", "text");
let row = build_row_from_scan(key, value, &[], Some(&schema))
.expect("row must be built (not tombstoned)");
assert_eq!(
row.values.get("id"),
Some(&Value::Text("k0000000000000000".to_string())),
"Issue #586: single TEXT PK column must be reconstructed from the raw row key"
);
assert_eq!(
row.values.get("name"),
Some(&Value::Text("name-0".to_string()))
);
}
#[test]
fn scan_built_row_matches_text_pk_equality_predicate() {
use super::super::super::select_optimizer::{SSTableFilterOp, SSTablePredicate};
let key = RowKey::new(b"k0000000000000000".to_vec());
let value = ScanRow::Row(vec![(Arc::from("age"), Value::Integer(0))]);
let schema = single_pk_schema("id", "text");
let row = build_row_from_scan(key, value, &[], Some(&schema)).unwrap();
let predicate = SSTablePredicate::column(
"id",
SSTableFilterOp::Equal,
vec![Value::Text("k0000000000000000".to_string())],
);
assert!(
evaluate_predicates(&row, std::slice::from_ref(&predicate)).unwrap(),
"Issue #586: WHERE id = '<literal>' must match the reconstructed PK column"
);
}
#[test]
fn build_row_from_scan_multi_column_row_has_no_data_fallback() {
let key = RowKey::new(b"k0000000000000000".to_vec());
let value = ScanRow::Row(vec![
(Arc::from("name"), Value::Text("alice".to_string())),
(Arc::from("score"), Value::Integer(42)),
]);
let row = build_row_from_scan(key, value, &[], None)
.expect("a live row must build (not tombstoned)");
assert_eq!(
row.values.get("name"),
Some(&Value::Text("alice".to_string())),
"real text column value must survive the row-carrier disassembly"
);
assert_eq!(
row.values.get("score"),
Some(&Value::Integer(42)),
"real int column value must survive the row-carrier disassembly"
);
assert!(
!row.values.contains_key("data"),
"roborev H2: column values must NOT collapse into a synthetic 'data' fallback"
);
assert_eq!(
row.values.len(),
2,
"exactly the two real columns, no extras"
);
}
#[test]
fn build_row_from_scan_presizes_value_map() {
let cells: Vec<(Arc<str>, Value)> = (0..8)
.map(|i| (Arc::from(format!("c{i}").as_str()), Value::Integer(i)))
.collect();
let key = RowKey::new(b"k".to_vec());
let row = build_row_from_scan(key, ScanRow::Row(cells), &["c0".to_string()], None)
.expect("a live row must build");
assert_eq!(row.values.len(), 1, "projection keeps exactly one column");
assert!(
row.values.capacity() >= 8,
"issue #1584: value map must be pre-sized to the decoded cell count \
(>= 8), not grown from empty to the projected size; got capacity {}",
row.values.capacity()
);
}
#[test]
fn build_row_from_scan_marker_is_suppressed() {
let key = RowKey::new(b"k".to_vec());
assert!(
build_row_from_scan(key, ScanRow::Marker(Value::Null), &[], None).is_none(),
"a marker (tombstone/null) row must be suppressed from user output"
);
}
#[test]
fn live_value_dropped_as_marker_surfaces_as_row() {
let key = RowKey::new(b"k".to_vec());
let live = Value::Text("synthetic-fallback".to_string());
assert!(
build_row_from_scan(key.clone(), ScanRow::Marker(live.clone()), &[], None).is_none(),
"a LIVE value mis-wrapped as Marker is dropped — the bug the producers must avoid"
);
let row = build_row_from_scan(
key,
ScanRow::Row(vec![(Arc::from("data"), live.clone())]),
&[],
None,
)
.expect("a live Row must surface");
assert_eq!(row.values.get("data"), Some(&live));
}
}