Skip to content

bindings/c: convert non-text values to text in sqlite3_value_text - #10852

Open
abrahamperz wants to merge 2 commits into
tursodatabase:mainfrom
abrahamperz:fix/c-value-text-coerce-fu1
Open

abrahamperz wants to merge 2 commits into
tursodatabase:mainfrom
abrahamperz:fix/c-value-text-coerce-fu1

Conversation

@abrahamperz

@abrahamperz abrahamperz commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #10728, and stacked on it: the first commit here is that PR's NUL-termination, which drops out once that PR merges to main.

A C function registered with sqlite3_create_function_v2 that read an argument with sqlite3_value_text got a null pointer when the argument was an INTEGER, REAL or BLOB. SQLite converts the value to text there, so strlen or printf on the result works for any type. The same accessor is used by sqlite3_value_dup copies and sqlite3_column_value, so they had the gap too.

-- a C scalar f that returns strlen(sqlite3_value_text(argv[0])):
SELECT f(42), f(1.5), f(X'414243');
-- sqlite:  2, 3, 3
-- main:    null pointer for all three
-- this PR: 2, 3, 3

Root cause

sqlite3_value_text returned a pointer only when the value already held text; to_text() gives None for a number or a blob. Turso has the bytes but never converted them.

Fix

Coerce the value to text in place, the way SQLite does, so the returned pointer stays valid: a number becomes its decimal text, a blob keeps its bytes verbatim, each NUL-terminated. from_text_bytes builds the text value for a blob without a UTF-8 round-trip. Any previous heap is freed before the text replaces it.

Replacing the value in place is safe because the ExtValue is owned for the duration of the call and freed by type afterwards: a scalar function's arguments live in a Vec that finish_call frees, column_value caches owned values in the statement, and a dup'd value is owned by the caller. So the allocation and its free stay matched.

One visible consequence, which matches SQLite: after sqlite3_value_text reads a value, a later sqlite3_value_type on it reports TEXT.

Testing

test_value_text_coerces_non_text_like_sqlite registers a C function that returns strlen(sqlite3_value_text(argv[0])) and checks that 42, 1.5, X'414243' and 'hello' give 2, 3, 3 and 5. It runs against both Turso and real SQLite. Without the change the numeric and blob cases return a null pointer, so -1 from the test.

sqlite3_value_text must return a pointer to a NUL-terminated string (the
SQLite API contract), but the C compat layer handed back the pointer of a
Rust &str whose backing buffer (TextValue, built by new_boxed) was exactly
`len` bytes with no terminator. A C caller running strlen on it read past
the buffer.

Allocate TextValue storage with a trailing NUL (len + 1 bytes); `len` still
counts only the text, so to_text()/as_str() are unchanged. free() releases
len + 1, and the error-message path (new_with_message) allocates the same
way so its stored message is also a valid C string.

Tests: text_value_pointer_is_nul_terminated checks the byte at `len` is NUL
for several inputs. Without the fix that read runs past the buffer (SIGSEGV);
with it, the test passes.

Fixes tursodatabase#7982
sqlite3_value_text returned a null pointer for an INTEGER, REAL or BLOB
value, while SQLite converts the value to its text form. A C function that
ran strlen on the result, or printed it, saw nothing for a numeric or blob
argument. This also reached sqlite3_value_dup copies and
sqlite3_column_value values, which read through the same accessor.

Coerce the value to text in place, as SQLite does, so the returned pointer
stays valid: a number becomes its decimal text and a blob keeps its bytes
verbatim, each NUL-terminated. from_text_bytes builds the text value for a
blob without a UTF-8 round-trip. Any previous heap is freed before the text
replaces it.

Follow-up to tursodatabase#10728.

Tests: test_value_text_coerces_non_text_like_sqlite registers a C function
that returns strlen(sqlite3_value_text(argv[0])) and checks 42, 1.5,
X'414243' and 'hello' give 2, 3, 3 and 5, matching SQLite. The numeric and
blob cases return -1 without the change.
@abrahamperz
abrahamperz marked this pull request as ready for review October 26, 2026 15:10
@abrahamperz
abrahamperz requested a review from penberg as a code owner October 26, 2026 15:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant