Skip to content

Commit 2e29fbd

Browse files
authored
Merge pull request #156 from TabularisDB/155-bug-postgresql-submit-the-modification-error-report
feat(postgres): add binding module for parameterized values
2 parents da5778d + f15a268 commit 2e29fbd

4 files changed

Lines changed: 587 additions & 198 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
use super::helpers::{
2+
escape_identifier, extract_base_type, is_raw_sql_function, is_wkt_geometry,
3+
json_array_to_pg_literal, try_parse_pg_array,
4+
};
5+
use tokio_postgres::types::ToSql;
6+
7+
pub(super) type PgParam = Box<dyn ToSql + Send + Sync>;
8+
9+
pub(super) struct BoundValue {
10+
pub sql: String,
11+
pub param: Option<PgParam>,
12+
}
13+
14+
pub(super) struct PgValueOptions<'a> {
15+
pub column_type: Option<&'a str>,
16+
pub max_blob_size: u64,
17+
pub allow_default: bool,
18+
}
19+
20+
/// Build a parameterized "<pk_col> = $N" predicate plus the boxed parameter for the
21+
/// given JSON pk_val. Numeric values are cast through bigint/double precision so the
22+
/// bind succeeds against int2/int4/int8/real columns; UUID strings are bound as the
23+
/// `Uuid` type so PostgreSQL receives the matching OID.
24+
pub(super) fn build_pk_predicate(
25+
pk_col: &str,
26+
pk_val: serde_json::Value,
27+
placeholder_idx: usize,
28+
) -> Result<(String, PgParam), String> {
29+
let col = format!("\"{}\"", escape_identifier(pk_col));
30+
match pk_val {
31+
serde_json::Value::Number(n) => {
32+
let bound = bind_pg_number(&n, placeholder_idx)?;
33+
let param = bound
34+
.param
35+
.ok_or_else(|| "Internal PostgreSQL numeric binding error".to_string())?;
36+
Ok((format!("{} = {}", col, bound.sql), param))
37+
}
38+
serde_json::Value::String(s) => {
39+
if let Ok(uuid) = s.parse::<uuid::Uuid>() {
40+
Ok((format!("{} = ${}", col, placeholder_idx), Box::new(uuid)))
41+
} else {
42+
Ok((format!("{} = ${}", col, placeholder_idx), Box::new(s)))
43+
}
44+
}
45+
_ => Err("Unsupported PK type".into()),
46+
}
47+
}
48+
49+
pub(super) fn bind_pg_value(
50+
value: serde_json::Value,
51+
placeholder_idx: usize,
52+
options: PgValueOptions<'_>,
53+
) -> Result<BoundValue, String> {
54+
match value {
55+
serde_json::Value::Number(n) => bind_pg_number(&n, placeholder_idx),
56+
serde_json::Value::String(s) => bind_pg_string(&s, placeholder_idx, options),
57+
serde_json::Value::Bool(b) => Ok(BoundValue {
58+
sql: format!("${}", placeholder_idx),
59+
param: Some(Box::new(b)),
60+
}),
61+
serde_json::Value::Null => Ok(BoundValue {
62+
sql: "NULL".to_string(),
63+
param: None,
64+
}),
65+
serde_json::Value::Array(arr) => Ok(BoundValue {
66+
sql: json_array_to_pg_literal(&arr)?,
67+
param: None,
68+
}),
69+
_ => Err("Unsupported Value type".into()),
70+
}
71+
}
72+
73+
/// SQL fragment + boxed parameter for a JSON Number bound to PostgreSQL.
74+
///
75+
/// tokio-postgres binds Rust `i64` as INT8 and `f64` as FLOAT8, and rejects the
76+
/// bind when the column is INT2/INT4/REAL with "error serializing parameter X".
77+
/// Wrapping the placeholder in `CAST($N AS bigint)` / `CAST($N AS double precision)`
78+
/// lets PostgreSQL convert to the actual column width via its assignment / implicit
79+
/// comparison casts.
80+
pub(super) fn bind_pg_number(
81+
n: &serde_json::Number,
82+
placeholder_idx: usize,
83+
) -> Result<BoundValue, String> {
84+
if let Some(v) = n.as_i64() {
85+
Ok(BoundValue {
86+
sql: format!("CAST(${} AS bigint)", placeholder_idx),
87+
param: Some(Box::new(v)),
88+
})
89+
} else if let Some(v) = n.as_f64() {
90+
Ok(BoundValue {
91+
sql: format!("CAST(${} AS double precision)", placeholder_idx),
92+
param: Some(Box::new(v)),
93+
})
94+
} else {
95+
Err(format!("Unsupported numeric value: {}", n))
96+
}
97+
}
98+
99+
pub(super) fn bind_pg_numeric_string(
100+
s: &str,
101+
column_type: &str,
102+
placeholder_idx: usize,
103+
) -> Option<Result<BoundValue, String>> {
104+
let trimmed = s.trim();
105+
let normalized = extract_base_type(column_type).to_lowercase();
106+
107+
if matches!(
108+
normalized.as_str(),
109+
"smallint" | "integer" | "bigint" | "int2" | "int4" | "int8" | "serial" | "bigserial"
110+
) {
111+
return Some(trimmed.parse::<i64>().map_or_else(
112+
|e| {
113+
Err(format!(
114+
"Cannot convert value {:?} to PostgreSQL numeric column type {}: {}",
115+
s, column_type, e
116+
))
117+
},
118+
|v| {
119+
Ok(BoundValue {
120+
sql: format!("CAST(${} AS bigint)", placeholder_idx),
121+
param: Some(Box::new(v) as PgParam),
122+
})
123+
},
124+
));
125+
}
126+
127+
if matches!(normalized.as_str(), "numeric" | "decimal") {
128+
return Some(trimmed.parse::<rust_decimal::Decimal>().map_or_else(
129+
|e| {
130+
Err(format!(
131+
"Cannot convert value {:?} to PostgreSQL numeric column type {}: {}",
132+
s, column_type, e
133+
))
134+
},
135+
|v| {
136+
Ok(BoundValue {
137+
sql: format!("CAST(${} AS numeric)", placeholder_idx),
138+
param: Some(Box::new(v) as PgParam),
139+
})
140+
},
141+
));
142+
}
143+
144+
if matches!(
145+
normalized.as_str(),
146+
"real" | "double precision" | "float4" | "float8"
147+
) {
148+
return Some(trimmed.parse::<f64>().map_or_else(
149+
|e| {
150+
Err(format!(
151+
"Cannot convert value {:?} to PostgreSQL numeric column type {}: {}",
152+
s, column_type, e
153+
))
154+
},
155+
|v| {
156+
Ok(BoundValue {
157+
sql: format!("CAST(${} AS double precision)", placeholder_idx),
158+
param: Some(Box::new(v) as PgParam),
159+
})
160+
},
161+
));
162+
}
163+
164+
None
165+
}
166+
167+
fn bind_pg_string(
168+
s: &str,
169+
placeholder_idx: usize,
170+
options: PgValueOptions<'_>,
171+
) -> Result<BoundValue, String> {
172+
if options.allow_default && s == "__USE_DEFAULT__" {
173+
return Ok(BoundValue {
174+
sql: "DEFAULT".to_string(),
175+
param: None,
176+
});
177+
}
178+
179+
if let Some(bytes) = crate::drivers::common::decode_blob_wire_format(s, options.max_blob_size) {
180+
return Ok(BoundValue {
181+
sql: format!("${}", placeholder_idx),
182+
param: Some(Box::new(bytes)),
183+
});
184+
}
185+
186+
if let Some(binding) = options
187+
.column_type
188+
.and_then(|data_type| bind_pg_numeric_string(s, data_type, placeholder_idx))
189+
{
190+
return binding;
191+
}
192+
193+
if is_raw_sql_function(s) {
194+
return Ok(BoundValue {
195+
sql: s.to_string(),
196+
param: None,
197+
});
198+
}
199+
200+
if is_wkt_geometry(s) {
201+
return Ok(BoundValue {
202+
sql: format!("ST_GeomFromText(${})", placeholder_idx),
203+
param: Some(Box::new(s.to_string())),
204+
});
205+
}
206+
207+
if s.parse::<uuid::Uuid>().is_ok() {
208+
return Ok(BoundValue {
209+
sql: format!("CAST(${} AS uuid)", placeholder_idx),
210+
param: Some(Box::new(s.to_string())),
211+
});
212+
}
213+
214+
if let Some(pg_arr) = try_parse_pg_array(s) {
215+
return Ok(BoundValue {
216+
sql: pg_arr?,
217+
param: None,
218+
});
219+
}
220+
221+
Ok(BoundValue {
222+
sql: format!("${}", placeholder_idx),
223+
param: Some(Box::new(s.to_string())),
224+
})
225+
}

0 commit comments

Comments
 (0)