Skip to content

Commit cc02661

Browse files
committed
fix: bind numeric SQL params as text when an explicit cast is present
The runner has its own bind_sql_primitive used by env.DB.query in workers (postgate is only used here for SQL parsing/validation, not for binding). Apply the same workaround as postgate v0.1.6 here: when the SQL has an explicit cast for a numeric param (e.g. \$N::int, \$N::real), bind it as text and let Postgres parse via the cast. Bypasses sqlx's prepared-statement type cache that intermittently mishandles binary i64 / f64 params, producing "invalid byte sequence for encoding UTF8: 0x00". Reuses postgate::has_explicit_cast (newly exposed in v0.1.7) instead of duplicating the helper.
1 parent 1889258 commit cc02661

3 files changed

Lines changed: 46 additions & 24 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openworkers-runner"
3-
version = "0.14.4"
3+
version = "0.14.5"
44
edition = "2024"
55
license = "MIT"
66
default-run = "openworkers-runner"
@@ -73,7 +73,7 @@ openworkers-runtime-wasm = { git = "https://github.com/openworkers/openworkers-
7373

7474
# Database bindings (optional)
7575
# postgate = { path = "../postgate", default-features = false, optional = true }
76-
postgate = { git = "https://github.com/openworkers/postgate", tag = "v0.1.6", default-features = false, optional = true }
76+
postgate = { git = "https://github.com/openworkers/postgate", tag = "v0.1.7", default-features = false, optional = true }
7777

7878
# Disabled runtimes (require openworkers-core 0.5, we're on 0.9)
7979
# openworkers-runtime-deno = { git = "https://github.com/openworkers/openworkers-runtime-deno", tag = "v0.5.1", optional = true }

src/services/database.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ pub async fn execute_json_query(
149149
let wrapped = wrap_query_as_json(sql, mode);
150150
let mut query = sqlx::query(&wrapped);
151151

152-
for param in params {
153-
query = bind_sql_param(query, param);
152+
for (i, param) in params.iter().enumerate() {
153+
query = bind_sql_param(query, param, sql, i + 1);
154154
}
155155

156156
let row = query
@@ -177,8 +177,8 @@ pub async fn execute_json_query_tx(
177177
let wrapped = wrap_query_as_json(sql, mode);
178178
let mut query = sqlx::query(&wrapped);
179179

180-
for param in params {
181-
query = bind_sql_param(query, param);
180+
for (i, param) in params.iter().enumerate() {
181+
query = bind_sql_param(query, param, sql, i + 1);
182182
}
183183

184184
let row = query
@@ -201,8 +201,8 @@ pub async fn execute_mutation(
201201
) -> Result<String, String> {
202202
let mut query = sqlx::query(sql);
203203

204-
for param in params {
205-
query = bind_sql_param(query, param);
204+
for (i, param) in params.iter().enumerate() {
205+
query = bind_sql_param(query, param, sql, i + 1);
206206
}
207207

208208
let result = query
@@ -221,8 +221,8 @@ pub async fn execute_mutation_tx(
221221
) -> Result<String, String> {
222222
let mut query = sqlx::query(sql);
223223

224-
for param in params {
225-
query = bind_sql_param(query, param);
224+
for (i, param) in params.iter().enumerate() {
225+
query = bind_sql_param(query, param, sql, i + 1);
226226
}
227227

228228
let result = query
@@ -237,9 +237,11 @@ pub async fn execute_mutation_tx(
237237
pub fn bind_sql_param<'q>(
238238
query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
239239
param: &'q SqlParam,
240+
sql: &str,
241+
param_idx: usize,
240242
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
241243
match param {
242-
SqlParam::Primitive(p) => bind_sql_primitive(query, p),
244+
SqlParam::Primitive(p) => bind_sql_primitive(query, p, sql, param_idx),
243245
SqlParam::Array(arr) => {
244246
// Determine array type from first non-null element
245247
let first_type = arr.iter().find_map(|p| match p {
@@ -306,12 +308,32 @@ pub fn bind_sql_param<'q>(
306308
pub fn bind_sql_primitive<'q>(
307309
query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
308310
param: &'q SqlPrimitive,
311+
sql: &str,
312+
param_idx: usize,
309313
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
310314
match param {
311315
SqlPrimitive::Null => query.bind(None::<String>),
312316
SqlPrimitive::Bool(b) => query.bind(*b),
313-
SqlPrimitive::Int(i) => query.bind(*i),
314-
SqlPrimitive::Float(f) => query.bind(*f),
317+
// When the SQL has an explicit cast for this param (e.g. $N::int), bind the
318+
// number as text and let Postgres parse via the cast. Binding as i64/f64 sends
319+
// 8-byte big-endian binary on the wire, which combined with sqlx's prepared-
320+
// statement type cache can intermittently be re-decoded as UTF-8 by Postgres,
321+
// producing "invalid byte sequence for encoding UTF8: 0x00" on numeric portal
322+
// parameters. Routing through the cast keeps the wire pure ASCII.
323+
SqlPrimitive::Int(i) => {
324+
if postgate::has_explicit_cast(sql, param_idx) {
325+
query.bind(i.to_string())
326+
} else {
327+
query.bind(*i)
328+
}
329+
}
330+
SqlPrimitive::Float(f) => {
331+
if postgate::has_explicit_cast(sql, param_idx) {
332+
query.bind(f.to_string())
333+
} else {
334+
query.bind(*f)
335+
}
336+
}
315337
SqlPrimitive::String(s) => query.bind(s.as_str()),
316338
}
317339
}

0 commit comments

Comments
 (0)