Skip to content

Say what the caller did wrong, and prove it in a suite - #13

Merged
tamnd merged 1 commit into
mainfrom
misuse
Aug 22, 2026
Merged

Say what the caller did wrong, and prove it in a suite#13
tamnd merged 1 commit into
mainfrom
misuse

Conversation

@tamnd

@tamnd tamnd commented Aug 22, 2026

Copy link
Copy Markdown
Owner

This is the misuse item on the zu-java scorecard, which the clients spec words as: deliberately wrong programs, and for each one no crash, no leak, and a clear message.

zudb-tck/src/main/java/dev/zudb/tck/MisuseTest.java is a table of thirty four wrong programs. Each row names what the caller did, the exception class it should raise, and the sentence it should say, and the factory turns each row into its own dynamic test so a failure names the mistake rather than naming a line number in a loop. It lives in the shared cases module, so both the Panama provider and the JNI provider run all of it.

Asserting the class alone would have been the easy version and it would have been worthless. A client that answers every mistake with zu_result_col_i64 answered MISUSE passes a class assertion. So the text is asserted too, as a substring, chosen to be the part a reader would act on.

Six cases did not pass when the table was first written, and this fixes those.

Result.longs, Result.doubles and Result.nodeOffsets now check what the column holds before they borrow it. Reading a column of strings as integers used to answer zu_result_col_i64 answered MISUSE, which is a sentence about our implementation. It now says which column it is, what it holds, and what the accessor reads.

Appender now knows the connection it writes through, and refuses with "the connection this appender writes through is closed" rather than passing the call down to an engine that refuses it without an error record attached, which left the caller reading the name of a C function.

Result.row now checks the result is open before it builds a Row. It used to hand back a row that failed on the first cell read, so the exception pointed at the wrong call.

Diagnostic.toException maps CORRUPT to ZuConnectionException instead of letting it fall through to ZuInternalException. A header that says something impossible could be a database this process corrupted and it could be a JPEG somebody pointed at, and the second is what nearly all of them are. ZuInternalException tells a caller to open a bug about somebody else's code. The class doc on ZuConnectionException now says that this reading was chosen and why.

Database.open, Database.create, Connection.open, Connection.create, Loader.create, Connection.query, Connection.prepare and Connection.appender name their parameter when it is null, rather than passing it on to a call that trips over it further in.

The second half of the file is lifecycle, which no single wrong program catches. Five hundred failed opens, a thousand connections opened and closed, and a thousand failed statements, each with the count of the process's own open file descriptors taken either side. A client that leaks a handle per failure passes every case in the table above and falls over in a server after an afternoon. There is also a test of the programs that look like misuse and are not, closing twice, an appender that finished with nothing in it, a result with no rows read as a borrowed column, a transaction that did nothing and still rolls back, so that none of this turns into a client that refuses ordinary work.

DiagnosticTest moved with the mapping change, and gained the UNKNOWN case that is now the one landing in ZuInternalException.

Checked on a Linux box against a release build of the engine. MisuseTest is 41 tests green on both providers, and the full reactor is green: API, TCK, Panama, JNI, Arrow and benchmarks, in 2m45s. The suite was also checked to bite, by taking the type check back out of Result.longs and watching the case go red with zu_result_col_i64 answered MISUSE printed against the sentence it was supposed to say.

One case is deliberately not in the table. A MATCH on a label that does not exist answers no rows today rather than saying the label is unknown. That is an engine matter and not a client one, and putting it in would have meant asserting a message that nothing produces. The appender is the call that does refuse an unknown table, so that is the case the table carries.

Thirty four deliberately wrong programs, each one asserting the class it
raises and the sentence it says, run by both providers. The bar is the one
dx/15 section 4 sets: no crash, no leak, and a message that names what the
caller did rather than naming a C function.

Six of them did not clear that bar, so this fixes those too. A column read
as the wrong type said "zu_result_col_i64 answered MISUSE"; it now names
the column, what it holds and what the accessor reads. An appender whose
connection closed under it said "zu_appender_flush answered MISUSE_CLOSED";
it now says the connection is closed. A result read after it closed handed
back a Row that failed later on the first cell instead of refusing at the
call. A file that is not a database was an internal error, which asks the
caller to report a bug about a path they mistyped, and is now a connection
error beside the file that is not there. And the entry points take a null
path or a null statement with the parameter named rather than passing it on
to a call that will trip over it somewhere else.

The second half is lifecycle. Five hundred failed opens, a thousand
connections, a thousand failed statements, each with the count of open file
descriptors taken either side of them, because a client that leaks a handle
per failure passes every single wrong program above and falls over in a
server. There is also a test of the programs that look like misuse and are
not, so that none of this turns into a client that refuses ordinary work.

A MATCH on a label that does not exist is deliberately not in the table.
Today it answers no rows rather than saying so, which is an engine matter
rather than a client one, and asserting the message we want here would have
meant asserting a behaviour that does not exist.
@tamnd
tamnd merged commit 06c4aac into main Aug 22, 2026
9 of 17 checks passed
@tamnd
tamnd deleted the misuse branch August 22, 2026 09:27
tamnd added a commit to tamnd/zu that referenced this pull request Aug 22, 2026
Thirty four deliberately wrong programs in the shared cases module, run by
both providers, each asserting the class it raises and the sentence it
says. Six of them did not pass when the table was first written, so the
same change fixes a column read as the wrong type, an appender whose
connection closed under it, a result read after it closed, a file that is
not a database being called an internal error, and the entry points that
took a null without naming the parameter. The lifecycle half counts open
file descriptors either side of five hundred failed opens, a thousand
connections and a thousand failed statements.

tamnd/zu-java#13. That takes the practice score to 60, and what is left is
leaks, install and stability, which are apparatus, and api-map and perf,
which are reports the release collects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant