Skip to content

sf_core: add typed JSON parameter-binding builder - #1335

Open
zeroshade wants to merge 3 commits into
snowflakedb:mainfrom
zeroshade:sf-core-bindings-builder
Open

sf_core: add typed JSON parameter-binding builder#1335
zeroshade wants to merge 3 commits into
snowflakedb:mainfrom
zeroshade:sf-core-bindings-builder

Conversation

@zeroshade

Copy link
Copy Markdown
Contributor

This change adds a public sf_core::bindings module that constructs Snowflake parameter-binding JSON directly from typed Rust values. It introduces a ParamValue enum covering every Snowflake logical type the driver binds, and two builders: to_json_single for a single parameter set and to_json_arrays for array (multi-row) binding. The wire-text encoding for each value is byte-for-byte identical to the existing ODBC converters in odbc/src/conversion, so a statement bound through either path yields the same server-side value.

Until now sf_core accepted parameter bindings only as pre-serialized JSON bytes: BindingType::Json points at raw UTF-8 that the core validates and forwards verbatim. Every consumer and test therefore had to hand-assemble the {"1": {"type", "value"}} payload, duplicating the encoding rules and inviting subtle drift from the ODBC behavior. This module gives Rust callers a single typed entry point and one authoritative place for the encoding rules.

The design deliberately mirrors the ODBC converters rather than inventing a new format. A few rules that are easy to get wrong are centralized here: every value is emitted as a JSON string (never a bare number or boolean), TIMESTAMP_LTZ is tagged TEXT and rendered as a bare wall-clock literal, TIMESTAMP_TZ encodes epoch nanoseconds plus a +1440-biased offset, and a column that is entirely NULL is tagged ANY. INTERVAL_YEAR_MONTH and INTERVAL_DAY_TIME are accepted as pre-formatted literal strings — matching the ODBC WriteWire implementations, which are the identity for those types — and the accepted grammar is documented on the enum variants.

The primary risk is divergence from the ODBC encoders. That is mitigated by porting each encoding from the corresponding converter and locking the behavior down with tests built from the same expected values the ODBC path produces.

Testing: 43 unit tests cover every logical type, NULL handling, single and array binding, JSON string escaping, and the mixed-type and mismatched-length error paths, asserting on parsed JSON structure rather than key order. cargo build, cargo test, cargo clippy -D warnings, and cargo fmt --check are clean for the crate.

Add a public sf_core::bindings module that constructs Snowflake
parameter-binding JSON from typed Rust values via a ParamValue enum and
two builders (to_json_single for one row, to_json_arrays for array
binding). The wire-text encodings match the ODBC converters in
odbc/src/conversion byte-for-byte, so a query bound through either path
produces the same server-side value.

Until now sf_core accepted only pre-serialized JSON binding bytes
(BindingType::Json points at raw UTF-8), leaving each caller and test to
hand-write the {"1": {"type", "value"}} payload. The new builder gives
Rust callers a typed, reusable entry point and centralizes the encoding
rules: every value is emitted as a JSON string, TIMESTAMP_LTZ is tagged
TEXT, TIMESTAMP_TZ applies the +1440 offset bias, and a fully-null column
is tagged ANY. Year-month and day-time intervals are accepted as
pre-formatted literals and documented on their enum variants.

Covered by 43 unit tests spanning every logical type, NULL handling,
array binding, string escaping, and the mixed-type and mismatched-length
error paths. cargo build, test, clippy, and fmt are clean for the crate.
Address a code-review finding on the new sf_core::bindings builder.
ParamValue::TimestampTz exposes offset_minutes as an arbitrary i32, and
encode_tz computed offset_minutes + 1440 unchecked, which panics on
overflow in debug builds and wraps into a bogus wire offset in release.

Validate offset_minutes against the driver's legal +/-1439-minute range
(matching the ODBC converter) before applying the bias, returning the new
BindingError::TimestampTzOffsetOutOfRange for anything wider. The range
check makes the biased sum provably overflow-safe. Adds regression tests
for the i32::MAX rejection and the +/-1439 boundaries.
Address a second code-review finding on sf_core::bindings. encode_wallclock
zero-padded the absolute year and prepended a sign, so year -1 rendered as
"-0001". The ODBC put_year formatter it mirrors counts the sign within the
minimum width of four, producing "-001", so TIMESTAMP_LTZ bind text diverged
for proleptic negative years.

Format the signed year with format!("{:04}", year), which counts the sign in
the width and matches the ODBC output for every year while leaving positive
years unchanged. Adds a negative-year regression test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant