From d93baa409230ddf77ca1ce440b7f472300d16cf6 Mon Sep 17 00:00:00 2001 From: Adam J Esslinger Date: Wed, 16 Sep 2026 15:06:02 -0400 Subject: [PATCH] fix: bind an empty JSON array as the quoted '{}' literal (#86) json_array_to_pg_literal had no empty-array special case, so an empty JSON array [] produced ARRAY[] (an ARRAY constructor with no elements), which PostgreSQL rejects in many contexts -- a syntax error, or "cannot determine type of empty array" -- since the server can't infer an element type from zero elements. The builtin driver special-cases this to '{}', the canonical PostgreSQL empty-array literal the server always accepts regardless of context. Note: the issue's suggested snippet used a bare, unquoted {}, but that's itself a syntax error on its own (confirmed live: "syntax error at or near \"{\""). Checked the builtin's actual current source and its git history -- it has always quoted this as '{}', which I verified live works correctly. Matched that exactly rather than the issue's snippet. Both call sites that reach json_array_to_pg_literal (bind_pg_value's native Value::Array arm, and bind_pg_string's inline "[...]"-embedded-in- a-string parse) share this one function, so the fix covers both without duplication. Verified end-to-end against a live database: the issue's exact repro (INSERT of {"ids": []} into an int[] column) now succeeds and round-trips as an empty array rather than erroring; a non-empty array still binds unchanged (no regression); and the string-embedded "[]" path binds identically. Also confirmed a pre-existing, shared limitation carries over unchanged from the builtin: a uniform nested empty array like [[],[]] still hits PostgreSQL's own type-inference gap for ARRAY['{}', '{}'] (each '{}' looks like text to the parser) -- reproduced against bare psql with the builtin's exact algorithm, confirming this is inherited, not introduced by this fix, and out of scope for #86. --- src/binding.rs | 10 ++++++++++ src/binding_tests.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/binding.rs b/src/binding.rs index d9bb940..4cfee24 100644 --- a/src/binding.rs +++ b/src/binding.rs @@ -436,6 +436,16 @@ fn decode_blob_wire_format(value: &str) -> Option> { /// Convert a JSON array to a PostgreSQL `ARRAY[...]` literal string. /// Recursively handles nested arrays (multi-dimensional PG arrays). fn json_array_to_pg_literal(arr: &[Value]) -> Result { + // `ARRAY[]` (an ARRAY constructor with no elements) is only accepted + // when the target column type is unambiguous — in many contexts + // (e.g. INSERT ... VALUES) it's a syntax/type error. `'{}'` is the + // canonical PostgreSQL empty-array literal the server always accepts; + // it must be quoted — a bare `{}` is itself a syntax error. Matches the + // builtin driver's `json_array_to_pg_literal` exactly. + if arr.is_empty() { + return Ok("'{}'".to_string()); + } + let mut parts = Vec::with_capacity(arr.len()); for elem in arr { let part = match elem { diff --git a/src/binding_tests.rs b/src/binding_tests.rs index 25bb930..99bf693 100644 --- a/src/binding_tests.rs +++ b/src/binding_tests.rs @@ -48,6 +48,24 @@ mod bind_pg_value_tests { assert_eq!(bound.sql, "ARRAY[ARRAY[1, 2], ARRAY[3, 4]]"); } + #[test] + fn empty_array_binds_as_quoted_empty_literal_not_array_constructor() { + // ARRAY[] (an ARRAY constructor with no elements) is only accepted + // when the target column type is unambiguous -- in many contexts + // (e.g. plain INSERT ... VALUES) it's a syntax/type error. '{}' is + // the canonical PostgreSQL empty-array literal the server always + // accepts. See #86. + let bound = bind_pg_value(json!([]), 1, &BindOptions::default()).unwrap(); + assert_eq!(bound.sql, "'{}'"); + assert!(bound.param.is_none()); + } + + #[test] + fn nested_empty_array_binds_as_quoted_empty_literal_at_every_level() { + let bound = bind_pg_value(json!([[]]), 1, &BindOptions::default()).unwrap(); + assert_eq!(bound.sql, "ARRAY['{}']"); + } + #[test] fn string_array_escapes_single_quotes() { let bound = bind_pg_value(json!(["it's", "ok"]), 1, &BindOptions::default()).unwrap(); @@ -260,6 +278,15 @@ mod bind_pg_value_tests { assert!(bound.param.is_none()); } + #[test] + fn empty_array_literal_embedded_in_string_binds_as_quoted_empty_literal() { + // Same #86 guard, reached via bind_pg_string's inline array-literal + // parse ("[]") rather than a native JSON array value. + let bound = bind_pg_value(json!("[]"), 1, &BindOptions::default()).unwrap(); + assert_eq!(bound.sql, "'{}'"); + assert!(bound.param.is_none()); + } + #[test] fn plain_string_falls_through_to_text_binding() { let bound = bind_pg_value(json!("hello world"), 1, &BindOptions::default()).unwrap();