Skip to content

Commit 3745f36

Browse files
Fix: Preserve decimal/float values in INSERT and UPDATE statements (#630)
This pull request improves support for decimal/float values in SQL parsing and sharding logic, especially for `INSERT` and `UPDATE` statements. It introduces a new `Float` variant to the `Value` and `AssignmentValue` enums, ensures decimals are preserved (not coerced to integers or null), and adds handling and tests for these cases. Additionally, it prevents the use of float/decimal columns as sharding keys, returning clear errors or routing to all shards when necessary. **Decimal/Float Value Handling Improvements:** * Added a `Float` variant to the `Value` and `AssignmentValue` enums, ensuring decimal values are preserved as strings throughout parsing and assignment (`pgdog/src/frontend/router/parser/value.rs`, `pgdog/src/frontend/router/parser/rewrite/shard_key.rs`). [[1]](diffhunk://#diff-bd5edaf0e41ddd3af4577a1e7e53af5953d62db4ef5e6531db38dccaf0c87b91R15) [[2]](diffhunk://#diff-7f25c6defa22d112850e0f942520463df6cd1d80af656ec435abcdeea3cd4f99R11) * Updated parsing logic to map float/decimal literals to the new `Float` variant instead of attempting to parse as integers or returning null (`pgdog/src/frontend/router/parser/value.rs`). * Modified assignment and value handling to correctly propagate and stringify float values in query rewriting and sharding logic (`pgdog/src/frontend/client/query_engine/shard_key_rewrite.rs`, `pgdog/src/frontend/router/parser/insert.rs`). [[1]](diffhunk://#diff-fabbbd1dac63eca483df4ee327365faebbcf441cb474b442c97e7db89d4bf854R424) [[2]](diffhunk://#diff-55ec6e26408c3763f93de5033481bdd1c9a202cfd351f48338a53d554c60a9d5R377) [[3]](diffhunk://#diff-314df04f8984799d59642fa3ea4027c9536ee9783f7a68ff7b01ddae3c5b80b3R323) **Sharding Logic and Error Handling:** * Updated sharding logic to reject float/decimal columns as sharding keys for split inserts, returning a clear error message, and to route updates with float sharding keys to all shards (`pgdog/src/frontend/router/parser/insert.rs`, `pgdog/src/frontend/router/parser/query/update.rs`). [[1]](diffhunk://#diff-55ec6e26408c3763f93de5033481bdd1c9a202cfd351f48338a53d554c60a9d5R247-R256) [[2]](diffhunk://#diff-314df04f8984799d59642fa3ea4027c9536ee9783f7a68ff7b01ddae3c5b80b3R266-R270) **Testing and Validation:** * Added comprehensive tests to verify that decimal values are preserved (not quoted unless originally quoted), that quoted decimals are treated as strings, and that float sharding keys are rejected or handled safely in both insert and update scenarios (`pgdog/src/frontend/router/parser/insert.rs`, `pgdog/src/frontend/router/parser/query/update.rs`). [[1]](diffhunk://#diff-55ec6e26408c3763f93de5033481bdd1c9a202cfd351f48338a53d554c60a9d5R892-R1074) [[2]](diffhunk://#diff-314df04f8984799d59642fa3ea4027c9536ee9783f7a68ff7b01ddae3c5b80b3R385-R470)
1 parent 42da082 commit 3745f36

File tree

5 files changed

+290
-7
lines changed

5 files changed

+290
-7
lines changed

pgdog/src/frontend/client/query_engine/shard_key_rewrite.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ fn apply_assignments(
421421

422422
let new_value = match assignment.value() {
423423
AssignmentValue::Integer(value) => Some(value.to_string()),
424+
AssignmentValue::Float(value) => Some(value.clone()),
424425
AssignmentValue::String(value) => Some(value.clone()),
425426
AssignmentValue::Boolean(value) => Some(value.to_string()),
426427
AssignmentValue::Null => None,

pgdog/src/frontend/router/parser/insert.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,16 @@ impl<'a> Insert<'a> {
244244
.shards(schema.shards)
245245
.build()?
246246
.apply()?,
247+
Value::Float(_) => {
248+
return Err(Error::SplitInsertNotSupported {
249+
table: table.name.to_owned(),
250+
reason: format!(
251+
"row {} uses a float/decimal value for sharding column \"{}\", which is not supported as a sharding key",
252+
tuple_index + 1,
253+
key.table.column
254+
),
255+
})
256+
}
247257
Value::Null => {
248258
return Err(Error::SplitInsertNotSupported {
249259
table: table.name.to_owned(),
@@ -364,6 +374,7 @@ impl<'a> Insert<'a> {
364374
) -> Result<StdString, Error> {
365375
match value {
366376
Value::Integer(int) => Ok(int.to_string()),
377+
Value::Float(float) => Ok((*float).to_string()),
367378
Value::String(string) => Ok(format_literal(string)),
368379
Value::Boolean(boolean) => Ok(if *boolean {
369380
StdString::from("TRUE")
@@ -878,4 +889,187 @@ mod test {
878889
_ => panic!("not an insert"),
879890
}
880891
}
892+
893+
#[test]
894+
fn split_insert_preserves_decimal_values() {
895+
let query = parse(
896+
"INSERT INTO transactions (txn_id, user_id, amount, status) VALUES \
897+
(1001, 1, 50.00, 'completed'), \
898+
(1002, 2, 20.00, 'failed'), \
899+
(1003, 3, 25.75, 'pending')",
900+
)
901+
.unwrap();
902+
let select = query.protobuf.stmts.first().unwrap().stmt.as_ref().unwrap();
903+
let schema = ShardingSchema {
904+
shards: 2,
905+
tables: ShardedTables::new(
906+
vec![ShardedTable {
907+
name: Some("transactions".into()),
908+
column: "user_id".into(),
909+
..Default::default()
910+
}],
911+
vec![],
912+
),
913+
..Default::default()
914+
};
915+
916+
match &select.node {
917+
Some(NodeEnum::InsertStmt(stmt)) => {
918+
let insert = Insert::new(stmt);
919+
let routing = insert
920+
.shard(&schema, None, true, RewriteMode::Rewrite)
921+
.unwrap();
922+
923+
match routing {
924+
InsertRouting::Split(plan) => {
925+
let rows = plan.rows();
926+
// Check that decimal values are preserved without quotes
927+
assert_eq!(rows[0].values()[2], "50.00");
928+
assert_eq!(rows[1].values()[2], "20.00");
929+
assert_eq!(rows[2].values()[2], "25.75");
930+
931+
// Verify strings are quoted
932+
assert_eq!(rows[0].values()[3], "'completed'");
933+
assert_eq!(rows[1].values()[3], "'failed'");
934+
assert_eq!(rows[2].values()[3], "'pending'");
935+
}
936+
InsertRouting::Routed(_) => panic!("expected split plan"),
937+
}
938+
}
939+
_ => panic!("not an insert"),
940+
}
941+
}
942+
943+
#[test]
944+
fn split_insert_with_quoted_decimal_values() {
945+
let query = parse(
946+
"INSERT INTO transactions (txn_id, user_id, amount, status) VALUES \
947+
(1001, 1, '50.00', 'completed'), \
948+
(1002, 2, '20.00', 'failed'), \
949+
(1003, 3, '25.75', 'pending')",
950+
)
951+
.unwrap();
952+
let select = query.protobuf.stmts.first().unwrap().stmt.as_ref().unwrap();
953+
let schema = ShardingSchema {
954+
shards: 2,
955+
tables: ShardedTables::new(
956+
vec![ShardedTable {
957+
name: Some("transactions".into()),
958+
column: "user_id".into(),
959+
..Default::default()
960+
}],
961+
vec![],
962+
),
963+
..Default::default()
964+
};
965+
966+
match &select.node {
967+
Some(NodeEnum::InsertStmt(stmt)) => {
968+
let insert = Insert::new(stmt);
969+
let routing = insert
970+
.shard(&schema, None, true, RewriteMode::Rewrite)
971+
.unwrap();
972+
973+
match routing {
974+
InsertRouting::Split(plan) => {
975+
let rows = plan.rows();
976+
// Quoted decimals should be preserved as strings
977+
assert_eq!(rows[0].values()[2], "'50.00'");
978+
assert_eq!(rows[1].values()[2], "'20.00'");
979+
assert_eq!(rows[2].values()[2], "'25.75'");
980+
981+
// Verify strings are quoted
982+
assert_eq!(rows[0].values()[3], "'completed'");
983+
assert_eq!(rows[1].values()[3], "'failed'");
984+
assert_eq!(rows[2].values()[3], "'pending'");
985+
}
986+
InsertRouting::Routed(_) => panic!("expected split plan"),
987+
}
988+
}
989+
_ => panic!("not an insert"),
990+
}
991+
}
992+
993+
#[test]
994+
fn debug_decimal_parsing() {
995+
let query = parse(
996+
"INSERT INTO transactions (txn_id, user_id, amount, status) VALUES \
997+
(1001, 101, 50.00, 'completed')",
998+
)
999+
.unwrap();
1000+
let select = query.protobuf.stmts.first().unwrap().stmt.as_ref().unwrap();
1001+
1002+
match &select.node {
1003+
Some(NodeEnum::InsertStmt(stmt)) => {
1004+
let insert = Insert::new(stmt);
1005+
let tuples = insert.tuples();
1006+
println!("Tuples: {:?}", tuples);
1007+
assert_eq!(tuples.len(), 1);
1008+
println!("Values: {:?}", tuples[0].values);
1009+
}
1010+
_ => panic!("not an insert"),
1011+
}
1012+
}
1013+
1014+
#[test]
1015+
fn reproduce_decimal_null_issue() {
1016+
let query = parse(
1017+
"INSERT INTO transactions (txn_id, user_id, amount, status) VALUES \
1018+
(1001, 101, 50.00, 'completed')",
1019+
)
1020+
.unwrap();
1021+
let select = query.protobuf.stmts.first().unwrap().stmt.as_ref().unwrap();
1022+
1023+
match &select.node {
1024+
Some(NodeEnum::InsertStmt(stmt)) => {
1025+
let insert = Insert::new(stmt);
1026+
let tuples = insert.tuples();
1027+
println!("Values: {:?}", tuples[0].values);
1028+
1029+
// After fix, this should be Float("50.00"), not Null
1030+
assert_eq!(tuples[0].values[2], Value::Float("50.00"));
1031+
}
1032+
_ => panic!("not an insert"),
1033+
}
1034+
}
1035+
1036+
#[test]
1037+
fn split_insert_rejects_float_sharding_key() {
1038+
let query = parse(
1039+
"INSERT INTO transactions (txn_id, amount, status) VALUES \
1040+
(1001, 50.00, 'completed'), \
1041+
(1002, 20.00, 'failed')",
1042+
)
1043+
.unwrap();
1044+
let select = query.protobuf.stmts.first().unwrap().stmt.as_ref().unwrap();
1045+
let schema = ShardingSchema {
1046+
shards: 2,
1047+
tables: ShardedTables::new(
1048+
vec![ShardedTable {
1049+
name: Some("transactions".into()),
1050+
column: "amount".into(),
1051+
..Default::default()
1052+
}],
1053+
vec![],
1054+
),
1055+
..Default::default()
1056+
};
1057+
1058+
match &select.node {
1059+
Some(NodeEnum::InsertStmt(stmt)) => {
1060+
let insert = Insert::new(stmt);
1061+
let result = insert.shard(&schema, None, true, RewriteMode::Rewrite);
1062+
1063+
match result {
1064+
Err(Error::SplitInsertNotSupported { table, reason }) => {
1065+
assert_eq!(table, "transactions");
1066+
assert!(reason.contains("float/decimal"));
1067+
assert!(reason.contains("not supported as a sharding key"));
1068+
}
1069+
other => panic!("expected error for float sharding key, got {:?}", other),
1070+
}
1071+
}
1072+
_ => panic!("not an insert"),
1073+
}
1074+
}
8811075
}

pgdog/src/frontend/router/parser/query/update.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ impl QueryParser {
263263
.build()?;
264264
Ok(context_builder.apply()?)
265265
}
266+
AssignmentValue::Float(_) => {
267+
// Floats are not supported as sharding keys
268+
// Return Shard::All to route to all shards (safe but not optimal)
269+
Ok(Shard::All)
270+
}
266271
AssignmentValue::String(text) => {
267272
let context_builder = ContextBuilder::new(sharded_table)
268273
.data(text.as_str())
@@ -315,6 +320,7 @@ impl QueryParser {
315320
if let Ok(value) = Value::try_from(&val.node) {
316321
return match value {
317322
Value::Integer(i) => Ok(AssignmentValue::Integer(i)),
323+
Value::Float(f) => Ok(AssignmentValue::Float(f.to_owned())),
318324
Value::String(s) => Ok(AssignmentValue::String(s.to_owned())),
319325
Value::Boolean(b) => Ok(AssignmentValue::Boolean(b)),
320326
Value::Null => Ok(AssignmentValue::Null),
@@ -376,6 +382,92 @@ mod tests {
376382
let column = QueryParser::res_target_column(target).expect("column");
377383
assert_eq!(column, "id");
378384
}
385+
386+
#[test]
387+
fn update_preserves_decimal_values() {
388+
let parsed = pgdog_plugin::pg_query::parse(
389+
"UPDATE transactions SET amount = 50.00, status = 'completed' WHERE id = 1",
390+
)
391+
.expect("parse");
392+
393+
let stmt = parsed
394+
.protobuf
395+
.stmts
396+
.first()
397+
.and_then(|node| node.stmt.as_ref())
398+
.and_then(|node| node.node.as_ref())
399+
.expect("statement node");
400+
401+
let update = match stmt {
402+
NodeEnum::UpdateStmt(update) => update,
403+
_ => panic!("expected update stmt"),
404+
};
405+
406+
// Check that we can extract assignment values including decimals
407+
let mut found_decimal = false;
408+
let mut found_string = false;
409+
410+
for target in &update.target_list {
411+
if let Some(NodeEnum::ResTarget(res)) = &target.node {
412+
if let Some(val) = &res.val {
413+
if let Ok(value) = Value::try_from(&val.node) {
414+
match value {
415+
Value::Float(f) => {
416+
assert_eq!(f, "50.00");
417+
found_decimal = true;
418+
}
419+
Value::String(s) => {
420+
assert_eq!(s, "completed");
421+
found_string = true;
422+
}
423+
_ => {}
424+
}
425+
}
426+
}
427+
}
428+
}
429+
assert!(found_decimal, "Should have found decimal value");
430+
assert!(found_string, "Should have found string value");
431+
}
432+
433+
#[test]
434+
fn update_with_quoted_decimal() {
435+
let parsed =
436+
pgdog_plugin::pg_query::parse("UPDATE transactions SET amount = '50.00' WHERE id = 1")
437+
.expect("parse");
438+
439+
let stmt = parsed
440+
.protobuf
441+
.stmts
442+
.first()
443+
.and_then(|node| node.stmt.as_ref())
444+
.and_then(|node| node.node.as_ref())
445+
.expect("statement node");
446+
447+
let update = match stmt {
448+
NodeEnum::UpdateStmt(update) => update,
449+
_ => panic!("expected update stmt"),
450+
};
451+
452+
// Quoted decimals should be treated as strings
453+
let mut found_string = false;
454+
for target in &update.target_list {
455+
if let Some(NodeEnum::ResTarget(res)) = &target.node {
456+
if let Some(val) = &res.val {
457+
if let Ok(value) = Value::try_from(&val.node) {
458+
match value {
459+
Value::String(s) => {
460+
assert_eq!(s, "50.00");
461+
found_string = true;
462+
}
463+
_ => {}
464+
}
465+
}
466+
}
467+
}
468+
}
469+
assert!(found_string, "Should have found string value");
470+
}
379471
}
380472

381473
impl QueryParser {

pgdog/src/frontend/router/parser/rewrite/shard_key.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use super::super::table::OwnedTable;
88
pub enum AssignmentValue {
99
Parameter(i32),
1010
Integer(i64),
11+
Float(String),
1112
String(String),
1213
Boolean(bool),
1314
Null,

pgdog/src/frontend/router/parser/value.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::net::{messages::Vector, vector::str_to_vector};
1212
pub enum Value<'a> {
1313
String(&'a str),
1414
Integer(i64),
15+
Float(&'a str),
1516
Boolean(bool),
1617
Null,
1718
Placeholder(i32),
@@ -53,13 +54,7 @@ impl<'a> From<&'a AConst> for Value<'a> {
5354
}
5455
Some(Val::Boolval(b)) => Value::Boolean(b.boolval),
5556
Some(Val::Ival(i)) => Value::Integer(i.ival as i64),
56-
Some(Val::Fval(Float { fval })) => {
57-
if let Ok(val) = fval.parse() {
58-
Value::Integer(val)
59-
} else {
60-
Value::Null
61-
}
62-
}
57+
Some(Val::Fval(Float { fval })) => Value::Float(fval.as_str()),
6358
_ => Value::Null,
6459
}
6560
}

0 commit comments

Comments
 (0)