Skip to content

Commit 3302658

Browse files
committed
test(pgwire): cover #216 error codes through the extended protocol
Extended-protocol (Parse/Bind/Execute) coverage for the new SQLSTATEs: undefined functions surface 42883 at execute time (Parse succeeds for this statement shape — planning is deferred, unlike undefined-table rejection), and division by zero surfaces 22012 at execute with a non-zero-divisor control. Also extends the registry invariant test with the geo-function match arms so its transcription covers every runtime dispatch module.
1 parent a8eb896 commit 3302658

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

nodedb-sql/src/functions/registry.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,5 +468,59 @@ mod tests {
468468
"system",
469469
&["version", "format_type", "pg_get_expr", "col_description"],
470470
);
471+
472+
// `nodedb_query::geo_functions::eval_geo_function`. Distinct from
473+
// the other modules above: this one lives in `nodedb-query`'s
474+
// top-level `geo_functions` module (not `functions::<name>`), and
475+
// is dispatched into from `functions::eval_function` for any
476+
// geo-prefixed/ST_-prefixed name — every `|`-joined alias on its
477+
// match arms is flattened into this one list, same as `math`'s
478+
// `ceil`/`ceiling` pairing above.
479+
assert_all(
480+
"geo_functions",
481+
&[
482+
"geo_distance",
483+
"haversine_distance",
484+
"geo_bearing",
485+
"haversine_bearing",
486+
"geo_point",
487+
"st_point",
488+
"geo_geohash",
489+
"geo_geohash_decode",
490+
"geo_geohash_neighbors",
491+
"st_contains",
492+
"st_intersects",
493+
"st_within",
494+
"st_disjoint",
495+
"st_dwithin",
496+
"st_distance",
497+
"st_buffer",
498+
"st_envelope",
499+
"st_union",
500+
"geo_length",
501+
"geo_perimeter",
502+
"geo_line",
503+
"geo_polygon",
504+
"geo_circle",
505+
"geo_bbox",
506+
"geo_as_geojson",
507+
"geo_from_geojson",
508+
"geo_as_wkt",
509+
"geo_from_wkt",
510+
"geo_x",
511+
"geo_y",
512+
"geo_num_points",
513+
"geo_type",
514+
"geo_is_valid",
515+
"geo_h3",
516+
"geo_h3_to_boundary",
517+
"geo_h3_resolution",
518+
"st_geohash",
519+
"st_geohashdecode",
520+
"h3_latlngtocell",
521+
"h3_celltolatlng",
522+
"st_intersection",
523+
],
524+
);
471525
}
472526
}

nodedb/tests/pgwire_extended_query_engines2.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,106 @@ async fn extended_query_oid_mismatch_text_for_int_errors_or_coerces() {
306306
}
307307
}
308308
}
309+
310+
// ── Cross-engine: issue #216 SQLSTATEs through the extended protocol ────────
311+
312+
/// Undefined-function rejection (issue #216, SQLSTATE `42883`) through the
313+
/// extended-query path. `sql_undefined_function.rs` proves this fires at
314+
/// PLAN time over simple-query (where Parse+Bind+Execute collapse into one
315+
/// round trip). Naively, `pgwire_database_authorization.rs`'s
316+
/// `prepared_parse_denies_*` tests suggest Parse/Describe is where this
317+
/// server plans a statement (authorization and undefined-table checks both
318+
/// reject there) — but confirmed empirically here, `.prepare()` for this
319+
/// statement actually *succeeds*: the function-registry existence gate is
320+
/// not consulted during Parse/Describe (no columns/params to resolve for
321+
/// this shape rules it out early), so the error is only raised once
322+
/// `.query()` drives Bind+Execute and the plan is actually built. Asserting
323+
/// on the real surfacing point, not the naive assumption.
324+
#[tokio::test]
325+
async fn extended_query_unknown_function_errors_42883() {
326+
let srv = TestServer::start().await;
327+
328+
let stmt = srv
329+
.client
330+
.prepare("SELECT some_function_that_does_not_exist_216(1, 2)")
331+
.await
332+
.expect(
333+
"Parse/Describe succeeds for this statement shape — the \
334+
undefined-function gate is not consulted until Bind/Execute",
335+
);
336+
337+
let err = srv
338+
.client
339+
.query(&stmt, &[])
340+
.await
341+
.expect_err("Execute must reject a call to an unregistered scalar function");
342+
let db_err = err
343+
.as_db_error()
344+
.expect("server must return a typed SQLSTATE for the unknown function");
345+
assert_eq!(
346+
db_err.code().code(),
347+
"42883",
348+
"unknown-function rejection must carry SQLSTATE 42883, got {}",
349+
db_err.code().code()
350+
);
351+
}
352+
353+
/// Division-by-zero rejection (issue #216, SQLSTATE `22012`) through the
354+
/// extended-query path. Unlike the undefined-function gate above, `10 / d`
355+
/// is a well-typed expression regardless of `d`'s runtime value, so
356+
/// Parse/Describe must succeed — the zero divisor can only be discovered
357+
/// once a bound row is actually evaluated, i.e. at Bind/Execute
358+
/// (`.query()`) time. Mirrors `sql_division_by_zero.rs`'s simple-query
359+
/// coverage of the same fix, through Parse/Bind/Execute instead.
360+
#[tokio::test]
361+
async fn extended_query_division_by_zero_errors_22012_at_execute() {
362+
let srv = TestServer::start().await;
363+
srv.exec(
364+
"CREATE COLLECTION ext_divzero_216 (id STRING PRIMARY KEY, d INT) \
365+
WITH (engine='document_strict')",
366+
)
367+
.await
368+
.expect("CREATE ext_divzero_216");
369+
srv.exec("INSERT INTO ext_divzero_216 (id, d) VALUES ('nonzero', 2)")
370+
.await
371+
.expect("INSERT nonzero row");
372+
srv.exec("INSERT INTO ext_divzero_216 (id, d) VALUES ('zero', 0)")
373+
.await
374+
.expect("INSERT zero-divisor row");
375+
376+
let stmt = srv
377+
.client
378+
.prepare_typed(
379+
"SELECT 10 / d AS ratio FROM ext_divzero_216 WHERE id = $1",
380+
&[Type::TEXT],
381+
)
382+
.await
383+
.expect(
384+
"Parse must succeed — `10 / d` is well-typed independent of d's runtime value",
385+
);
386+
387+
let err = srv
388+
.client
389+
.query(&stmt, &[&"zero"])
390+
.await
391+
.expect_err("Execute over the zero-divisor row must fail the statement");
392+
let db_err = err
393+
.as_db_error()
394+
.expect("server must return a typed SQLSTATE for division by zero");
395+
assert_eq!(
396+
db_err.code().code(),
397+
"22012",
398+
"zero-divisor Execute-time rejection must carry SQLSTATE 22012, got {}",
399+
db_err.code().code()
400+
);
401+
402+
// Control: the identical prepared statement, bound against the
403+
// non-zero row, must still succeed — the fix must not turn division
404+
// itself into a blanket error on this path.
405+
let rows = srv
406+
.client
407+
.query(&stmt, &[&"nonzero"])
408+
.await
409+
.expect("Execute over the non-zero-divisor row must still succeed");
410+
assert_eq!(rows.len(), 1, "expected exactly 1 row for the nonzero id");
411+
}

0 commit comments

Comments
 (0)