chore: scaffold standalone stackable-odbc-core repository - #1
Merged
Conversation
adwk67
previously approved these changes
Jul 24, 2026
… serve real rows Three more defaulted Backend methods, with core owning the column layout and the sort. SQLTablePrivileges orders by PRIVILEGE before GRANTEE, so its keys are not in ascending column order -- that is the spec, and a comment on the constant says so. The four rewrites left set_empty_result with no callers, so it is deleted. Each rewritten function now logs its parsed arguments at debug, matching SQLTablesW and SQLColumnsW rather than the bare entry log the stubs had. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… functions Every string argument of all four is an identifier under SQL_TRUE -- no TableType-style exemption in this family -- so core normalises all of them. The (DM) markers are not uniform, and that is the trap: all four state the METADATA_ID null-catalog clause unmarked, so all four check it, but only SQLColumnPrivileges states an unmarked, unconditional null-TableName sentence. SQLTablePrivileges, SQLProcedures and SQLProcedureColumns must not check one. Tests pin the difference in both directions, verified by mutation. Two existing tests passed a null TableName to SQLColumnPrivilegesW and now supply one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records what a driver author needs for the four new ones: why they default to an empty result set rather than NotImplemented, and why only SQLColumnPrivileges checks a null TableName. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQLColumnPrivileges' null-TableName HY009 is the only part of the new group an existing driver test suite can notice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQLPrimaryKeysW, SQLForeignKeysW, SQLStatisticsW and SQLSpecialColumnsW logged only the statement handle, so a log could show that a catalog call happened but not what it asked for -- the one thing worth knowing when a client's metadata query returns nothing. All ten catalog functions now follow AGENTS.md's shape: trace! the raw inputs at entry, debug! the parsed values after. Also corrects two comments that the new SQLColumnPrivileges check falsified: the unmarked null-TableName HY009 clause now belongs to three catalog functions, not two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rule read as being about raw integers becoming enums, so a function whose only parsing was UTF-16 pointers to strings looked compliant with a bare entry log. Four catalog functions were, until the commit before this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SqlState::operation_canceled, the defaulted Backend::is_cancelled hook, and src/cancel.rs holding the single reclassification helper the ~23 backend call sites will use. Only the error half is ever examined. The spec allows a cancelled execution to finish anyway -- "it is possible for the execution to succeed and return SQL_SUCCESS while the cancel is also successful" -- so Ok is returned untouched and a test pins that. All twelve mocks that implement Backend::cancel now implement is_cancelled too. Leaving them unpaired would have made the crate's own test doubles violate the rule the trait's doc comment states. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backend::cancel marks the token, so a token reused by the next execution stayed marked and every later call on that statement observed a cancellation that was not its own. The spec requires the opposite: "After the statement has been canceled, the application can call SQLExecute or SQLExecDirect again." The create-once rule this replaces was defending against a SQLCancel that reaches an already-finished execution and cancels nothing. The spec states that outcome is correct, not a bug -- "a call to SQLCancel when no processing is being done on the statement ... has is [sic] no effect at all" -- so it was defending the wrong property at the cost of a real one. Two existing tests asserted create-once and are inverted rather than deleted, each carrying the spec sentence that overturned it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tests Models "another thread cancelled this while it was in flight" by having the backend signal its own token from inside the call, immediately before failing. Core sees the same state a real cross-thread cancel produces -- a failed call whose token reads cancelled -- with none of the timing nondeterminism. The genuinely concurrent path is proved separately. Smoke-tested here rather than left for the entry-point tests to discover: a broken mock should fail in one place, not twenty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQLExecDirect, SQLPrepare, SQLExecute and SQLParamData ask the execution's cancel token on the error path and relabel the failure as HY008. The spec's HY008 row carries no (DM) marker on any of these pages, and its second clause is exactly this crate's cross-thread cancel. The Ok path is deliberately untouched: "it is possible for the execution to succeed and return SQL_SUCCESS while the cancel is also successful." The token comments these sites carry still described create-once, which Task 3 replaced; corrected here rather than left to mislead the next reader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each catalog call asks its execution's cancel token on the error path and relabels the failure, as the execution entry points already do. SQLStatistics and SQLSpecialColumns needed care the other eight did not: they convert the backend's error before matching so a NotImplemented can become the spec's empty result set. NotImplemented there means "this backend exposes no index metadata" -- an answer, not a failure -- so only the genuine-error arm is reclassified. Reclassifying the whole Result would turn a spec-mandated empty result set into HY008, and four existing tests catch that. SQLDescribeCol and SQLColAttribute are deliberately NOT wired; see the report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These consume a cursor an earlier execution opened, so they observe that execution's token rather than minting one. StatementBackend::fetch takes no token and deliberately still does not: coupling that trait to Backend::CancelToken would be a worse trade than resolving the token from the handle registry, which is what SQLCancel already does. Adds reclassify_cancelled_opt for the case these three have and the statement-producing calls do not -- no backend call has run yet, so there is no token and nothing that could have been cancelled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thread A blocks inside the backend holding the connection's group lock, thread B calls SQLCancel, A returns HY008. Every other HY008 test simulates the interleaving on one thread, which pins the reclassification but not the lock behaviour -- this one needs SQLCancel to actually take its try_lock-failed branch. The backend's wait is bounded rather than unconditional, which is what makes the mutation useful: replacing try_lock with lock makes SQLCancel wait for the call it was asked to cancel, and the test then fails in 10s with a clear message instead of hanging CI forever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every one claimed the state could not arise, on one of two false grounds: that "the Backend trait is synchronous", which says nothing about another thread cancelling, or that it was (DM)-handled, which the spec contradicts -- the HY008 row carries no (DM) marker on any of these pages. Each now names which of the row's two clauses applies, in one of three shapes: the call reclassifies (18), it is connection-level and has no token to observe (6), or it makes no fallible backend call for a cancellation to be reported through (8). SQLDescribeCol and SQLColAttribute get their own wording: they do reach a fallible call, but its map_err replaces any error with 07009, so a cancellation is indistinguishable from a bad column number by the time core sees it. Classifying each function rather than pattern-matching turned up one the design missed: SQLDescribeParam makes a fallible backend call and has a token in reach, so it is wired here rather than documented as exempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minting per execution made this interleaving reachable for the first time: set_cancel used to run at most once per statement, so nothing could race a replacement. Now every statement-producing call writes while sql_cancel reads with no group lock, serialised only by the registry's own RwLock. Which of the two tokens the canceller observes is deliberately not asserted -- both are correct, and the spec says so. What the model earns is that neither thread panics, the downcast always succeeds, and the read never yields None for a slot that has held a token throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects in the parameter path, fixed in this order because making an unbound marker an error before hardening the scanner would have turned working statements into errors. `count_params` tracked single-quoted string literals and nothing else, so a `?` inside a delimited identifier or a comment counted as a parameter marker: `SELECT "a?b" FROM t` reported one parameter and `SELECT 1 -- huh?` reported one too. `escape.rs` already scanned the same text correctly, so the fix is to stop having a second scanner: its `copy_*` helpers split into `skip_*` plus a copying wrapper, and `count_params` calls the same `skip_*` functions with the identifier delimiters taken from the backend's `EscapeDialect`. A marker with no binding was padded with `ColumnValue::Null`, so `WHERE x = ?` with nothing bound ran as `WHERE x = NULL` — no rows, and `SQL_SUCCESS`. Both execution paths now report 07002, the first clause of that row on the `SQLExecute` and `SQLExecDirect` tables, neither of them `(DM)`-marked. The data-at-execution scan rejects the same gap so it is not a second route to the old behaviour. A `SQL_PARAM_OUTPUT` binding still yields `Null`: it has no input value, and reading its uninitialised buffer would be unsound. `read_param_value` matched on the C type alone, so `ParameterBinding::sql_type` — `SQLBindParameter`'s `ParameterType` — was recorded and never read. For every C type but the two character ones that lost nothing; for `SQL_C_CHAR` and `SQL_C_WCHAR` it discarded the only statement of what the text was, and `SQL_C_CHAR` + `SQL_NUMERIC` reached the backend as a string. The new `param_convert` module is the spec's "C to SQL: Character" table transcribed, with the SQLSTATE its third column gives for each outcome. Decimal literals are carried as digits and a scale rather than through `f64`, so scale survives. Driver-visible: a backend now receives `Decimal`, `I32`, `Timestamp` and so on where a character binding previously produced `String`. Verified with `pre-commit run --all-files` (15 hooks), 937 unit tests, and Miri (927 passed, no leaks). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`text_to_sql_type` checked that character parameter data denoted the declared type but never measured it against `SQLBindParameter`'s `ColumnSize` and `DecimalDigits`, so `12.345` bound as `DECIMAL(10,2)` reached the backend whole and so did a thirty-digit value. Both are 22001 on the "C to SQL: Character" table — truncation of fractional digits, and loss of whole digits — and both are now returned. Two readings the spec does not supply, and the reasoning for each: A `ColumnSize` of 0 reads as "no size declared". The spec defines no sentinel for an unknown size, but no decimal has zero digits of precision, so 0 cannot be a literal declaration; taking it literally would reject every value an application that omits it ever binds. A negative `DecimalDigits` disables the check rather than reading as 0, because it asks for a rounding to tens or hundreds that core has none to apply. The check validates without reshaping: trailing zeros beyond the declared scale lose nothing and are not truncation, and the value is passed on as the application wrote it. Only `SQL_DECIMAL` and `SQL_NUMERIC` are checked. `ColumnSize` is a count of mantissa bits for the approximate numerics, whose row tests range instead and already does; for everything else the spec says plainly that "for other data types, the ColumnSize argument is ignored", which is why the integer targets keep the C type's own range check. Tests pin both of those directions. The declared size for character and binary targets is still unenforced. `text_to_sql_type`'s "Declared size" note records what is missing and why the character row cannot be answered without a `Backend` hook: its test is in bytes of the data source's own encoding, which core does not know. Verified with `pre-commit run --all-files` (15 hooks), 951 unit tests, and Miri (941 passed, no leaks). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…location SQLSetStmtAttr's Comments make SQL_ATTR_METADATA_ID one of exactly two attributes an application may set at the connection level. SQLSetConnectAttr stored it and SQLGetConnectAttr read it back, but nothing else looked: metadata_id_enabled consults the statement's own map, and a statement was allocated with an empty one. The connection-level route therefore returned SQL_SUCCESS, echoed the value back, and left every catalog call treating its arguments as search patterns rather than identifiers. A statement now starts from its connection's value, seeded at the one site that decides a statement's initial state. Per the ODBC 2.x rule this route inherits, it is the default for statements allocated afterwards; existing statements are untouched and a later SQLSetStmtAttr still overrides it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hem all back The spec's 01S02 row closes the set of statement attributes a driver may substitute for at eight: SQL_ATTR_CONCURRENCY, SQL_ATTR_CURSOR_TYPE, SQL_ATTR_KEYSET_SIZE, SQL_ATTR_MAX_LENGTH, SQL_ATTR_MAX_ROWS, SQL_ATTR_QUERY_TIMEOUT, SQL_ATTR_ROW_ARRAY_SIZE and SQL_ATTR_SIMULATE_CURSOR. Each now stores the value core actually uses and posts 01S02, routed through one substitute_stmt_attr helper so the rule has a single place to read. Attributes off that list take HYC00 instead, since there is no substitution to report: SQL_ATTR_USE_BOOKMARKS above SQL_UB_OFF, SQL_ATTR_RETRIEVE_DATA = SQL_RD_OFF, SQL_ATTR_CURSOR_SENSITIVITY = SQL_SENSITIVE, SQL_ATTR_ENABLE_AUTO_IPD = SQL_TRUE, and SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_ON. SQLGetStmtAttr gains arms for the nine attributes SQLSetStmtAttr stores but could not report, and a test drives readability off statement_attribute_from_raw rather than a hand-written list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQL_ATTR_PACKET_SIZE reports HY011 once the connection is open, which its spec entry states directly. SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_ON and SQL_ATTR_ENLIST_IN_DTC report HYC00: core is synchronous and reports SQL_AM_NONE for SQL_ASYNC_MODE, and it enlists in no distributed transaction. Unrecognized attributes are still accepted silently for DM and tool compatibility. SQLGetConnectAttr gains arms for SQL_ATTR_ASYNC_ENABLE and SQL_ATTR_TRANSLATE_OPTION, so every attribute the setter stores can be read back, with a test enumerating the pairs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQLSetStmtAttr stores SQL_ATTR_PARAMS_PROCESSED_PTR and SQL_ATTR_PARAM_STATUS_PTR, and SQLExecDirect, SQLExecute and the SQLParamData data-at-execution completion now write through them. The processed count is 1, since SQL_ATTR_PARAMSET_SIZE is pinned at 1, and the first status element is SQL_PARAM_SUCCESS, or SQL_PARAM_ERROR when the execution failed — written before the error propagates. This is the parameter-side counterpart of what SQLFetch writes through SQL_ATTR_ROWS_FETCHED_PTR and SQL_ATTR_ROW_STATUS_PTR, and shares its unaligned-write discipline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The classification test asks one question of every info type — does the answer move when the backend does? — but only over default_get_info. The raw path was unpoliced, and it is the only place several info types are ever answered: SQL_CURSOR_ROLLBACK_BEHAVIOR has no odbc_sys::InfoType variant at all, and SQL_TABLE_TERM, SQL_PROCEDURES and their neighbours have one but no arm in default_get_info. The sibling test scans the whole u16 range against the same two mocks and sorts what it finds into two lists. RAW_PATH_CORE_FACTS holds the five core is entitled to decide, each with its reason. RAW_PATH_GAPS holds the three that are claims about the data source — SQL_QUOTED_IDENTIFIER_CASE, SQL_MULTIPLE_ACTIVE_TXN and SQL_DATABASE_NAME — each naming the hook or derivation it still needs. Those three are outstanding work, and the list can only shrink: a new hard-coded data-source claim on this path fails the test rather than joining them silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Core owns these column layouts, so a column added to a spec result set should be a core-only change. #[non_exhaustive] is what makes that true: without it a new field breaks every struct literal in every driver. Rust rejects a struct expression for a non_exhaustive type outside its own crate, including ..Default::default() (E0639), so each type gains one consuming setter per column. The catalog_rows! macro generates the struct, the marker and the setters from a single field list, so the three cannot drift apart, and adding a column later adds a setter — an additive change. Setters take impl Into<T> and are named after their field: an Option<String> column accepts a bare String, a String column accepts a &str, a nullable numeric column accepts the bare number. No positional new(): ColumnRow has eighteen columns and ProcedureColumnRow nineteen, and an argument list would reintroduce the ordering mistake named fields exist to prevent. The module doctest is compiled as a separate crate, so it proves the idiom works from outside core rather than only inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…self Each is a falsifiable statement about the data source that core had hard-coded: quoted_identifier_case, txn_capable, integrity, multiple_active_txn, special_characters, accessible_procedures, and the identity four driver_name, driver_version, dbms_name and dbms_version. SQL_TXN_CAPABLE is the sharpest of them. It had no arm anywhere, so it took the shape-aware default of U16(0) — SQL_TC_NONE, "transactions not supported" — even for a backend declaring an isolation level and implementing end_tran. Backend::set_autocommit's own doc already referred to "a backend that reports SQL_TC_NONE for SQL_TXN_CAPABLE", a declaration no hook exposed. A test now pins it against txn_isolation_options in both directions. driver_name and driver_version take no connection, so core answers the whole pre-connect group the Windows Driver Manager asks for: SQL_DRIVER_NAME, SQL_DRIVER_VER, SQL_DRIVER_ODBC_VER, SQL_ASYNC_DBC_FUNCTIONS and SQL_MAX_CONCURRENT_ACTIVITIES. The AGENTS.md checklist item about overriding get_info_pre_connect for them is replaced by two declarations the compiler asks for. The guard lists shrink accordingly: three entries leave CORE_FACTS, five leave SHAPE_DEFAULT_IS_THE_ANSWER, and two of the three RAW_PATH_GAPS are closed, leaving only SQL_DATABASE_NAME's derivation from SQL_ATTR_CURRENT_CATALOG. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec makes SQL_DATABASE_NAME a second name for SQL_ATTR_CURRENT_CATALOG, so SQLGetInfo reads the attribute the connection already stores rather than answering "" from a second place. It cannot live in common_get_info_raw with the other raw-path answers, because the attribute is on the connection handle rather than on B::Connection, so sql_get_info_w threads it through. A backend that knows the real current database still wins: get_info_raw runs first. This closes the last RAW_PATH_GAPS entry. SQL_CURSOR_SENSITIVITY reports SQL_UNSPECIFIED. Insensitivity is a promise that no other cursor's changes ever become visible, and core's fetch streams rows as the application asks for them, so it can promise nothing about rows it has not read. The spec puts the two either side of a conformance line, but this describes core's cursor rather than the backend's SQL grammar, so it does not follow Backend::sql_conformance. SQLSetStmtAttr accepts only that value now, and SQLGetStmtAttr reports it, so the attribute and the info type agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
conformance::info_group_inconsistencies states the invariants once, in the
shared harness each driver's suite already runs against its real backend, and
returns one message per violation. Core cannot police a backend's get_info at
runtime — that method runs first and is entitled to answer anything — so this
is what makes the vendor-terminology group safe without Backend hooks: a driver
that answers SQL_CATALOG_TERM but leaves SQL_CATALOG_NAME saying "N" fails its
own tests.
Two invariants are one-directional on purpose. SQL_PROCEDURES = "Y" implies a
non-empty SQL_PROCEDURE_TERM but not the converse, because the info type is a
conjunction that includes the driver supporting {call}. An empty
SQL_SCHEMA_TERM implies SQL_SCHEMA_USAGE = 0 but not the converse, because the
SQL_SCHEMA_TERM page names an SQL_SCHEMA_NAME info type that does not exist in
sqlext.h, leaving the term as its own support signal.
observe_string_value and observe_u16_value join observe_u32_value so all three
value shapes can be read through the real entry point. Core's own answers are
asserted group-consistent, with four value assertions alongside so the
implications cannot pass vacuously on a reader that returns "" and 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ented write_utf16 collected a Vec<u16> before checking whether the caller had anywhere to write, so a pure length query built the units only to discard them. Counted instead, with the Vec built after that branch. translate_escapes returns brace-free input unchanged without allocating a Vec<char> as wide as the statement or running the scanner. Setup alone goes from 86 ns to 6 ns, before counting the scan now skipped, and it runs on every SQLExecDirect and SQLPrepare. text_without_braces_is_returned_unchanged pins that the fast path and the scanner agree, which is the property worth testing. The ten catalog row types gain a consuming into_values, measured at 0.40x the borrowing form for a 50 000-row result set, 3.28 ms against 1.33 ms. Implemented by inverting rather than adding: into_values is primary and to_values delegates to self.clone().into_values(), so the ten spec column orders are still written once. Ten hand-written into_values would have been a second copy of an ordering an application binds by number. logging.rs records the threat model the audit asked for, with each claim checked: a log holds connection parameters other than credentials, which ConnectParams redacts by hand under test, plus catalog filter arguments, and no statement text or parameter or column values — schema-revealing rather than data-revealing. ODBC_LOG_FILE is not a privilege boundary, since setting it requires already running as that user. Symlinks are followed, which leaves the world-writable-directory case operational rather than something core can decide. Mode 0o600 applies at creation only. CRLF-stripping for backend-originated text is left undecided and recorded as a question in the plan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 7.1's deferred Miri pass, which found three defects rather than confirming none. Miri runs at all only because cargo-miri's sysroot cache can be redirected with XDG_CACHE_HOME; the development sandbox denies ~/.cache/miri, so every earlier attempt on this branch died in "failed to build sysroot" before interpreting anything. Two task entries that said their pointer work was unverified under Miri are corrected: it is verified now. First, the job had been red since 814167b. connect_refuses_an_nts_credential_that_runs_to_the_scan_cap reaches read_dsn_keys, and Miri cannot call the foreign SQLGetPrivateProfileStringW. Its ServerName sibling is exempt because that scan overruns before the DSN read; this one parses the server name first. Three neighbouring SQLConnectW tests already carried the same ignore. Second, the budget, which answers the question raising MAX_NTS_SCAN left open. Eighteen boundary tests allocate a buffer of exactly the cap, and one of them takes 392 seconds interpreted, so eighteen is about two hours against timeout-minutes: 30. The cap is 1 << 12 under cfg(miri) rather than those tests being ignored, which keeps what they exist for: the buffer has no terminator, so a read one unit past it is a heap overflow Miri sees, and that holds at any cap. All eighteen now run in 37 seconds. Two tests assert an absolute 100 000-character input is accepted, which is false at 4096, and are ignored with the reason recorded at the constant — grepping for symbolic uses of MAX_NTS_SCAN missed them, and only the full run found them. Third, a real handle leak. Six leaked allocations, all from cursor.rs's cleanup_env_conn_stmt_for, which freed stmt, conn and env without disconnecting. free_connection refuses a still-open connection with HY010 and frees nothing, so every test there that connected leaked its connection handle and the HY010 message its diagnostic queue held. It predates this work and went unnoticed because Miri was deferred. Also fifteen MD012 violations, every one introduced by this plan's own outcome entries plus one in the CHANGELOG. markdownlint needs a Node environment the sandbox cannot install, so it never ran during the work — but the rule is ten lines to implement, which was the right move at any point instead of reporting the hook as unavailable. Both Miri passes are now clean: 1514 pass, no leaks, no undefined behaviour, and no alignment errors under -Zmiri-symbolic-alignment-check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prose says "Backend has 4 associated types" and the example directly above it showed three: `CancelToken` was missing, so a driver author following the sketch hit a compile error the surrounding text said not to expect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec makes HY091 and HYC00 different claims, and neither row carries a
Driver-Manager marker, so both are the driver's to return:
HY091 "was not one of the defined values and was not an
implementation-defined value"
HYC00 "was not supported by the driver"
Every unrecognised identifier answered HYC00, so an application could not tell
a garbage value from a valid extension core has not implemented.
SQLGetDescFieldW and SQLSetDescFieldW already drew this line through
field_from_raw; SQLColAttributeW was the outlier.
The three ODBC 2.x spellings -- SQL_COLUMN_LENGTH, SQL_COLUMN_PRECISION and
SQL_COLUMN_SCALE -- keep HYC00, because they are defined identifiers. They are
deliberately absent from desc_from_raw, which is shared with the descriptor
functions, where an ODBC 2.x column identifier names no field at all.
Supporting them outright is what the Backward Compatibility section asks of a
3.x driver and is tracked separately: their ODBC 2.x semantics differ from
their 3.x counterparts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
07009's clause naming "a column number greater than the number of columns in the result set" carries no (DM) marker, so it is the driver's. It was delegated to the backend, which answered whatever its error mapping produced -- usually HY000. The recorded reason was that "a precise check would require an extra round-trip to obtain the column count". That was false. StatementBackend::column_count is a local accessor with no I/O behind it, and describe_col has been range-checked against that same call all along. get_data_with_a_huge_ordinal_is_the_backends_hy000_not_07009 existed to pin the old behaviour "so that changing it is a decision rather than an accident". This is that decision; the test is renamed and inverted, and a boundary test covers the last valid column so the comparison cannot drift to >=. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQLBindParameter's StrLen_or_IndPtr defines five negative values: SQL_NTS,
SQL_NULL_DATA, SQL_DEFAULT_PARAM, SQL_DATA_AT_EXEC and
SQL_LEN_DATA_AT_EXEC(n). Both character arms of read_param_value folded every
negative into SQL_NTS, so SQL_NO_TOTAL, -6 and -42 bound the whole
null-terminated string and answered SUCCESS -- a value the application never
asked to send, with no diagnostic.
SQLExecDirect's and SQLExecute's HY090 rows state the condition themselves and
carry no (DM) marker for it, and SQLPutData already refused the same class.
SQL_DEFAULT_PARAM is handled beside SQL_NULL_DATA at the top of the function
rather than per-arm, so the character arms can treat every remaining negative
as undefined. It resolves to NULL, which is the ruling sql_put_data's doc
comment already recorded: it names a procedure parameter's default, and
crate::escape refuses {call ...} with HYC00, so no statement core executes has
one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Display Size appendix does not just give a number, it says what the number
is made of: SQL_FLOAT/SQL_DOUBLE is "24 (a sign, 15 digits, a decimal point,
the letter E, a sign, and 3 digits)" and SQL_REAL is "14 (a sign, 7 digits, a
decimal point, the letter E, a sign, and 2 digits)". Both describe an exponent
rendering, and col_attr::display_size_for already reported 24 and 14 quoting
those very sentences.
Rust's Display for floats never emits an exponent, so core promised a
24-character exponent form and produced a positional one up to 326 characters
long. Two outcomes, the second worse than the first:
f64::MAX 309 positional digits trip the SQL to C: Numeric whole-digit
rule, so a display-size buffer got a hard 22003 -- loud.
4.9e-324 326 characters whose first 24 are 0.00000000000000000000000, so
the application read zero under 01004 "truncated" -- silent, and
not the claim 01004 makes.
The switch is conditional, so 1.5 is still "1.5": the spec fixes the size, not
the notation, and rendering every float in exponent form would satisfy the
size while changing every value an application actually sees.
Neither neighbouring driver could be read end-to-end -- MySQL Connector/ODBC
formats through my_gcvt and psqlODBC passes PostgreSQL's own float8 text
through -- but both are consistent with a conditional switch. The deciding
argument is the spec's own definition of the display size, not the survey.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It had no arm in write_column_value at all, so every value answered 07006
through both SQLGetData and SQLBindCol+SQLFetch, while the same C type worked
as an input parameter. SQL_C_NUMERIC shares the SQL to C: Numeric table's
exact-integer row with SQL_C_SLONG and SQL_C_SBIGINT -- n/a, 01S07, 22003 --
and the overview page states drivers "are required to support conversions to
all ODBC C data types from the ODBC SQL data types that they support". It is
the natural C type for a DECIMAL column.
The conversion is a rescale and a base change on the digit string, not a
numeric conversion: SQL_NUMERIC_STRUCT is +/- val x 10^-scale with val an
unsigned little-endian magnitude, which is the shape DecimalLiteral already
has, so the digits never pass through f64.
BREAKING: write_column_value and write_column_value_at take a final
NumericTarget carrying the ARD's SQL_DESC_PRECISION and SQL_DESC_SCALE. The
spec makes those the application's way of declaring the struct's layout
("SQLSetDescField is required to perform manual binding with SQL_C_NUMERIC
values"), so the conversion cannot be done from the ColumnValue alone.
NumericTarget::UNSPECIFIED means the application declared nothing and the
value describes itself; zero is not a legal precision, which is what makes it
usable as that marker. A struct rather than two i16 parameters, because two
adjacent same-typed arguments can be crossed at a call site and still compile.
fetch.rs's Binding becomes a struct for the same reason, now that it carries
seven fields.
Tests cover to_numeric_struct directly -- the sign convention (odbc-sys
documents it as "1 if positive, 0 if negative", the opposite of a sign bit),
the rescale in both directions, the 01S07 flag and its zero-digit exemption,
and the u128 boundary from both sides. Writing them found that 39 nines does
*not* fit a u128 despite u128::MAX having 39 digits, so MAX_U128_DIGITS is
only a pre-expansion guard against a pathological exponent and the real bound
is the parse.
The usual misalignment test could not be written and the reason is recorded
instead: SQL_NUMERIC_STRUCT is u8/i8/[u8; 16], so it has alignment 1 and every
address is aligned for it. The test asserts that, and fails if odbc-sys ever
widens a field.
The fuzz target threads precision and scale through as fuzzed input rather
than constants; 12.3M executions clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ColumnValue::Guid converted to SQL_C_BINARY and SQL_C_CHAR but its own C type answered 07006. The SQL to C: GUID table gives it one row -- test "None", data written, indicator 16, no SQLSTATE -- and there is no failure case. The reported half of this did not survive checking. The claim was that "ODBC defines SQL_CHAR -> SQL_C_GUID, which should be 22018 on a bad parse rather than a blanket refusal". It does not: SQL_C_GUID appears in exactly one conversion table, whose only source type is SQL_GUID, and the SQL to C: Character table has no SQL_C_GUID row at all. The overview page prescribes 07006 for "an identifier for an ODBC C data type not shown in the table for a given ODBC SQL data type", so the existing refusal is correct and a 22018 would be inventing a cell the spec does not have. A test pins that direction too. SQLGUID's first three groups are integers whose textual form is the big-endian reading of the bytes, which is the order column_value_to_string already renders. Reading them natively would byte-swap the GUID on every little-endian machine, silently; a mutation of that line fails two tests. Unlike SQL_NUMERIC_STRUCT, SQLGUID leads with a u32 and so has alignment 4 -- it can genuinely be misaligned, and has the usual offset-by-one test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OutConnectionString carries credentials in cleartext, and that is deliberate rather than an oversight. The spec's own description is a "completed connection string" and applications persist it to reconnect without prompting; redacting it would hand back a string that no longer connects -- a silent failure at the next startup instead of a visible one now. Backend::sensitive_connect_keywords governs Debug redaction only, and the split is a threat-model difference rather than an inconsistency: a log file is written to a path the application did not choose and read by people who never held the credential, while the echo goes back to the caller that supplied the string. A driver author must still declare credentials there so they stay out of logs, and they will still appear in the echo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StatementBackend::take_value_warning, defaulted to None and drained by core
immediately after every get_data.
Core already raises 01S07 where it drops precision -- a non-zero
ColumnValue::Time fraction written to SQL_C_TYPE_TIME, or a fraction lost
reaching an exact-integer C type. A driver that loses precision inside its own
type conversion does so before a ColumnValue exists, so core never saw it: a
driver reporting decimal_digits = 12 for a timestamp(12) column and delivering
nine answered SQL_SUCCESS with no diagnostic, and could not fix it
driver-side.
Both call sites that read a value drain it -- SQLGetData and SQLFetch's
bound-column loop -- because a warning visible through only one of them is
invisible to every application that uses SQLBindCol. Deleting either drain
fails its own test.
Three points of shape:
- Option, not Vec: get_data is the hottest path in the crate and a
collection would allocate per column per row to carry nothing.
- A separate method, not a widened get_data return: the Cow return exists so
caching backends hand back a borrow without cloning.
- take_, not get_: core calls it once per value and expects it cleared. The
mock clears on read so a test can tell "drained per value" from "reported
forever", and warns on one column of two so the warning is provably
attached to the value rather than the statement.
StatementData forwards it; a synthetic result set has none, because core
builds those rows from values it already holds.
This is the first mechanism by which a backend attaches a diagnostic to a
value it produced successfully. It is not an error channel -- a returned
warning does not make the call fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/superpowers/ was checked in by mistake; plans and specs live in the gitignored .superpowers/ tree. scripts/ held one hook, no-duplicate-test-names.sh, wired in through .pre-commit-config.yaml and so also run by the pre-commit CI job. Both the script and the hook entry are removed together, so nothing references a path that no longer exists. Note this retires a real guard: a test function carrying two #[test] attributes is registered twice and its neighbour, whose attribute was absorbed, silently stops running. Both outcomes leave cargo test green, so the registered names were the only place either showed up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ver reports
Nine changes from twelve reported items. Three reports did not survive
checking and are recorded as such rather than acted on:
- The f64 rendering claim named the wrong symptom. Core already answered a
hard 22003 for f64::MAX; the real defect was subnormals delivering zero
under 01004, and the justification is the Display Size appendix defining
SQL_DOUBLE's 24 characters as an exponent form.
- SQL_CHAR -> SQL_C_GUID is not a defined conversion. The SQL to C:
Character table has no SQL_C_GUID row, so the existing 07006 is correct
and the proposed 22018 would have invented a spec cell.
- SQLGetTypeInfo's empty result set for an invalid type is deliberate and
left alone; changing it needs a driver survey first.
Two defects found that were not reported: SQL_COLUMN_LENGTH/PRECISION/SCALE
are absent from desc_from_raw though the spec requires a 3.x driver to
support them (now correctly HYC00, still unimplemented), and SQL_DEFAULT_PARAM
was unhandled on the bound-parameter path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ease The three documents read as a development log rather than as documentation. This rewrites them for a reader who has not been following the work. CHANGELOG.md collapses from 252 KB to 8 KB. Every one of its 292 entries measured a delta from an unpublished monorepo that no consumer ever had, and the five migration sections described breaking changes against that same baseline. It becomes an initial capability statement instead. README.md addresses driver authors throughout, keeping a short ODBC primer for a reader evaluating the crate. The testing commands and the trait sketch move to CONTRIBUTING.md and AGENTS.md, which removes three command blocks that were duplicated byte for byte. Its links to AGENTS.md and fuzz/README.md become absolute, because Cargo.toml excludes both from the published tarball. AGENTS.md is reordered so Architecture comes first rather than last, and so Descriptors and Concurrency are top-level sections rather than filing under Testing. The narration goes: incident reports, sentences about what an earlier revision did, and measured runtimes that had already gone stale once. Counts that grow with the code are replaced by pointers to the authoritative list. Counts the ODBC spec fixes stay. CONTRIBUTING.md and SECURITY.md are new. The issue-template config no longer says a security policy is missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The doc comments carried about 3,150 em-dashes, and a habit of narrating the code's own history. Both render on docs.rs, and neither helps a reader decide anything. Every one of the em-dashes is gone except three in types/diagnostics_table.rs, where the character is load-bearing: one is the bullet parser's own terminator set, and two document it. The narration goes with them. A doc comment now states the rule and the failure it prevents, rather than which revision introduced it or what the comment used to say. Where the history was carrying an argument, that argument is restated as a counterfactual: a token minted once per statement WOULD leave a cancelled statement unusable, rather than an account of the revision where it did. Counts that grow with the code are replaced by pointers to the authoritative list. Counts the ODBC spec fixes stay. Two of the removed counts were already wrong: ffi/fetch.rs described CORE_UNEXPORTED_FUNCTIONS as seventeen entries where it holds nineteen, of which only fifteen are Appendix G mappings, and a struct described as carrying seven fields has six. Dangling references to gitignored plan documents are gone: a docs/superpowers/plans/ path in column_value.rs, and the B3, C2, C5 and Task 2.10 prefixes that pointed at trackers a reader cannot open. Two structural fixes: ffi/stmt_attr.rs had the doc block for offer_to_data_source attached to struct Substitution, so the two rendered as one run-on item; and param_convert.rs's section dividers now match the style its two sibling conversion modules use. Comments only. The 40 changed non-comment lines are all continuation lines of multi-line assertion messages or trailing comments on code lines. The diagnostics-table guard in types/diagnostics_table.rs parses these doc comments, so its four verdict phrasings, its SQLSTATE bullet shapes and its DM-attribution vocabulary were preserved exactly. pre-commit run --all-files passes, including cargo test and cargo doc with warnings denied. The Windows target and the detached bench/ and fuzz/ workspaces build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…th (DM) sql_get_type_info and sql_execute described their 24000 row as having a clause belonging to the Driver Manager. Neither row carries a (DM) marker anywhere. The three pages concerned, SQLExecDirect, SQLExecute and SQLGetTypeInfo, share a row shape the transcription cannot express: a first condition, then a sentence splitting that same condition by outcome, "returned by the Driver Manager if SQLFetch or SQLFetchScroll has not returned SQL_NO_DATA, and is returned by the driver if SQLFetch or SQLFetchScroll has returned SQL_NO_DATA", then further unattributed conditions. Both sides own the first condition, at different moments, and the driver owns the rest outright. Calling it the Driver Manager's clause was wrong in both directions: it gave away a case core does handle, and it counted the attribution sentence as a condition. SQLPrepare is the contrast and was already right. Its 24000 prints an actual (DM) on the first of two sentences, which is why that one is transcribed Split while these three are None. All four transcriptions were correct and are unchanged. Only the doc comments moved, plus sql_exec_direct_w's, which was accurate but silent about the split that its two neighbours now describe. types/diagnostics_table.rs records the shape and the limit it implies: the guard reads (DM) markers, so it cannot check a boundary the spec draws in English, and a row like this must not be promoted to Split to make the prose match, because Split names a marked clause and there is none. Verified by mutation: attributing the row to the Driver Manager in a phrasing the guard recognises makes every_doc_comment_matches_the_spec_diagnostics_table fail and name the function and the state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…SQLSTATEs Three changes to the test suite, none of which touches production code. Every diff hunk in all fifteen files is inside a `mod tests` block or in `test_utils.rs`. Eleven private re-implementations of helpers `test_utils.rs` already exported are gone: five byte-identical copies of `connect_handle`, six of a cleanup that was `cleanup_connected_env_conn_stmt` spelled out, and two shapes of the allocator. `test_utils.rs` gains the three the survey found missing, an unconnected generic triple, an environment-and-connection pair and its teardown, plus a shared `connect_handle`. The four cleanup copies had to keep disconnecting before freeing, and nothing stopped them drifting apart on that step. Getting it wrong leaks the connection and the environment, which fails as a red Miri job rather than as a failing assertion, so one copy is worth more than four. Twenty tests are removed, each proven redundant first by breaking the line it covered and confirming the surviving test failed. A byte-identical pair in `column_value.rs` and one in `cursor.rs`; two pairs in `metadata.rs` written twice, keeping the ones that pass named constants where the others passed bare 0; and sixteen in `col_attr.rs`, twelve display-size and octet-length assertions with a `constants_*` twin that additionally ties the result to its named constant, and four subsumed by parameterised tests already driving the same inputs through `get_column_attribute`. Four `col_attr.rs` tests the survey's table paired with nothing were kept. Deleting them would have dropped the SMALLINT and TINYINT octet-length branches and the VARCHAR display-size branch entirely. About forty tests now assert which SQLSTATE came back rather than only that the call failed. A test asserting `SqlReturn::ERROR` alone passes whether the driver returns the spec's state or a generic HY000, which for this crate is the assertion worth having. `metadata.rs`'s `first_sqlstate` now reads through `sql_get_diag_rec_w` instead of the diagnostic queue directly, so its twelve existing call sites assert what an application can observe too. Tests whose state comes from a backend were left alone, because `MockBackend` collapses everything to `NotImplemented` and asserting that pins the mock. Two weaknesses found while working, both closed: `display_size_bit` used a descriptor whose precision equalled the answer, so it passed whether the code returned `DISPLAY_SIZE_BIT` or fell back to the precision; and two of the `free_stmt` option tests carried comments claiming they checked the bindings were cleared while asserting only the return code. `SQLBindParameter` writes two descriptors, and the mutation showed the IPD half was genuinely unpinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SQLBindCol rejected SQL_C_DATE with HY021, "inconsistent descriptor information", for a bind that had nothing wrong with it. SQL_C_TIME and SQL_C_TIMESTAMP were accepted, so the three siblings behaved three different ways and none of it was chosen. The cause is a numeric collision. SQL_DATETIME is itself 9 and SQL_INTERVAL is 10, so verbose_type passes the 2.x codes through unmapped and they then compare equal to a verbose constant by identity rather than by mapping. 9 looks like a datetime whose subcode is missing and fails the consistency check, 10 looks like an interval and skips the check entirely, and 11 is an ordinary type that passes. verbose_type's comment anticipates the collision without following it through to what the check then does. 10 and 11 had a second problem behind the first: they passed the bind and then reached a fetch, where column_value has arms for the 3.x spellings only. set_concise_type now promotes 9, 10 and 11 to 91, 92 and 93. That is the Driver Manager's own mapping, from the two tables in "Datetime Data Type Changes", which give the same pair of values for the C type in SQLBindCol, SQLGetData and SQLBindParameter's ValueType and for the SQL type in its ParameterType. One translation therefore serves the ARD, the APD and the IPD. This is not core second-guessing the Driver Manager. Behind one, the deprecated codes never reach core at all, because that table maps them for an ODBC 3.x driver in both the "2.x app" and the "3.x app" column. Doing the same here extends the guarantee to an application linked directly against this crate, which it can be, and which core's own tests are. set_concise_type is the single writer of the type trio, so SQLBindCol, SQLBindParameter, SQLSetDescField and SQLSetDescRec all inherit it. Two tests. One pins the promotion and the consistency check for each of the three codes. The other sweeps every C data type c_data_type_from_raw accepts through a record shaped the way SQLBindCol shapes one and requires that none fails the check, which is the guard that would have caught this. It carries a miri ignore: it scans the whole i16 space and the module holds no unsafe. sql_bind_col's doc comment said HY021 was returned by this driver. The check does run there, but no argument the function accepts can now fail it, and the comment says so and names the two functions that can reach it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README's dependency snippet says `cargo add stackable-odbc-core` rather than pinning a version, so the rule matched nothing and `exactly = 1` turned that into a hard failure. A release aborted before it reached the commit. `Cargo.toml` now holds the only version string in the repository, so nothing outside it needs rewriting at release time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`setup.rs` was the one module missing from AGENTS.md's crate-layout table, which otherwise lists every file down to `ffi/mod.rs`. It is driver-facing API, `ConfigRequest`, `InstallerError` and `config_request_from_raw`, and it is what a driver reads before implementing `Backend::configure_dsn`. `release.toml` and `release/release.sh` both still described a release as rewriting README.md. The README version replacement was removed in 32e72d2, so only CHANGELOG.md is rewritten now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec's Default C Data Types table pairs SQL_GUID with SQL_C_GUID. Core inferred SQL_C_BINARY from the `ColumnValue::Guid` variant, and the two are not interchangeable even though both write sixteen bytes: SQL_C_GUID reassembles the first three groups as integers, so the SQL_C_BINARY reading byte-swaps them on every little-endian machine. An application binding SQL_C_DEFAULT on a GUID column allocates a SQLGUID and reads `d1` from it, so it received 0x33221100 where 0x00112233 was stored, under SQL_SUCCESS with nothing in the diagnostic queue. `default_target_width` gains the SQL_C_GUID row it now needs: that arm calls `write_fixed`, which does not consult BufferLength, because for an explicitly named fixed C type the spec has the driver ignore it. SQL_C_DEFAULT inverts that, so without the row the inference could write sixteen bytes into eight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
04b8428 made an undefined negative *StrLen_or_Ind* HY090 for SQL_C_CHAR and SQL_C_WCHAR and did not reach SQL_C_BINARY, which mapped every negative onto its no-indicator case and bound NULL. An application that passed SQL_NO_TOTAL, or a stale -42, sent a NULL to the data source in place of the value it bound, under SQL_SUCCESS and with an empty diagnostic queue. By the time control reaches these arms every negative names none of the values SQLBindParameter defines: SQL_NULL_DATA and SQL_DEFAULT_PARAM return at the top of `read_param_value`, and the data-at-execution values are diverted by `find_data_at_exec_params` before it is called. SQL_NTS is where the three arms legitimately part company, and the new test states the difference rather than one half of it: the character arms take it as "scan for the terminator", and a binary value has no terminator to find, so it names no length for SQL_C_BINARY and is refused with the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…NULL `read_param_value`'s terminal arm answered every C type it did not handle with `ColumnValue::Null` and a `tracing::warn!`. A log line is not a diagnostic, so the application's value was replaced by a NULL, sent to the data source and reported as SQL_SUCCESS with an empty diagnostic queue. Two families reached it. The thirteen SQL_C_INTERVAL_* types and the SQL_ARD_TYPE / SQL_APD_TYPE sentinels are genuinely unmarshallable and are now refused with 07006, at bind where the pairing is already fixed and again at execute, which is where a binding assembled through SQLSetDescField arrives without passing the bind gate. SQL_C_DEFAULT is the larger half and is not unmarshallable at all: the spec has it name its C type through the declared ParameterType. That table, the Default C Data Types page, is transcribed as `param_convert::default_c_type_for_parameter` and resolved before any arm runs. Two of its rows are easy to get wrong and are commented as such: the character rows say SQL_C_CHAR and not SQL_C_WCHAR even in a W driver, since the W governs a function's own string arguments rather than a bound buffer; and SQL_FLOAT resolves to SQL_C_DOUBLE, only SQL_REAL being single precision. The data-at-execution path had the same gap from the other side: `dae_buffer_to_value` matched the raw concise type, so a SQL_C_DEFAULT parameter declared SQL_LONGVARBINARY reached the text arm and had every non-UTF-8 byte replaced, where the same parameter sent in one piece was read as binary. The allowlist is positive so a C type absent from both it and `read_param_value` fails closed, costing a spurious 07006 rather than a silent NULL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every SQL_C_INTERVAL_* target and every exact-numeric target for an interval column answered 07006; only the character rows worked. The gap was undocumented, and `sql_fetch`'s doc comment claimed `write_column_value` returned 22015 while `sql_get_data`'s said the opposite about the same function. The two pages, SQL to C: Year-Month Intervals and SQL to C: Day-Time Intervals, are now transcribed in `column_value.rs`: the C interval targets (data / 01S07 for a truncated trailing-fields portion / 22015 for a leading precision too small), footnote [b]'s exact-numeric row, and the character and binary rows, which both pages word differently from every other source, giving 22003 where the others truncate with 01004. The two pages disagree in one cell. "Interval precision was not a single field" against an exact numeric is 22015 on the year-month page and 07006 on the day-time page. Each is answered with its own page's state, because a caller reads the page for the type it asked about. `ColumnValue`'s interval variants gain the precision the tables turn on: neither "is the precision a single field?" nor "which trailing fields did the source have?" can be answered from a magnitude. `IntervalDayTime` is now a signed i128 nanosecond total rather than i64 milliseconds. It stays one signed magnitude, for the reason the variant already documented, that split fields admit mixed signs and cannot express a negative sub-day interval; nanoseconds because SQL_INTERVAL_STRUCT's fraction counts billionths, and i128 because u32::MAX days of nanoseconds overflows i64. Rendering is now per-precision, from the spec's Interval Literals page: the leading field unpadded, trailing fields two digits, the fraction trimmed. An hour interval renders as "163" and not "0 05:00:00.000", which is what puts the character rows' whole-digit test on digits the value actually has. `NumericTarget` carries the ARD's SQL_DESC_DATETIME_INTERVAL_PRECISION alongside the precision and scale it already held, since all three are read from one record at one moment. A leading precision of 0 reads as "the application declared none", the convention `numeric_convert` already applied to this field in the C-to-SQL direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`SQL_YEAR_MONTH_STRUCT` is two SQLUINTEGERs where `SQL_DAY_SECOND_STRUCT` is five, so constructing the union through its year-month arm alone left the last twelve bytes uninitialised. The whole struct is then copied into the application's buffer, so those twelve bytes were driver stack handed to the caller: uninitialised memory an application can read back, and a disclosure however dull its contents. The union is now initialised through its widest arm first and only then overwritten, so every byte of the value is defined whichever target was asked for. Miri found it, as an uninitialised `DaySecond::minute` on the year-month tests, reading through a test helper whose SAFETY comment claimed it read only the arm the target wrote. It did not: building that struct reads both arms eagerly however few the caller goes on to inspect. The comment now records what the soundness actually rests on. The accompanying test asserts the post-condition but is explicitly not the guard: the skipped bytes come from the driver's own stack rather than from the destination buffer, so nothing the test controls decides their value and reverting the fix does not reliably fail it. Miri is what sees this, which is why it runs on every pull request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Extracts the database-independent ODBC framework from the
stackable-odbc-rsmonorepo into this standalone, crates.io-releasable library
Layout
src/,Cargo.toml);fuzz/is asubdir whose path dependency points back at
...Cargo.tomlis a plain[package]with inline[lints.clippy](
unwrap_used/unwrap_in_result/panic = "deny") — no workspace.Releasable to crates.io
description,repository,readme,keywords,categories,rust-version = 1.95.0,authors,license.Dependency decoupling
snafu,tracing,odbc-sys,tracing-subscriber,tracing-appender(+ devcriterion,proptest). No driver dependencies(
rusqlite,trino-rust-client,tokio, …) leak in — verified.deny.tomltrimmed: dropped the Trino-onlyRUSTSEC-2024-0436ignore, theringlicense clarify, and the Windows target; fixed the stale header.Tooling & CI
cargo-sortand thecheck-yaml/check-toml/check-merge-conflict/mixed-line-endinghygiene hooks; droppedshellcheck(no shell scripts). Manifests sorted accordingly.SQLite integration, Windows cross-compile, and driver-specific jobs. Windows
cross-compile removed entirely (core builds no DLL).
Docs
AGENTS.mdpartitioned to be driver-agnostic (no Trino/SQLite names);driver integration/stress/profiling sections removed; kept core architecture,
crate-layout, the odbc-sys/conversion guides, Miri/fuzz, and a generic
"adding a driver" walkthrough.
README.mdrewritten for the standalone crate.CHANGELOG.mdin Keep a Changelog format.Verification
All green:
cargo fmt,clippy -D warnings,cargo test(593),cargo doc -D warnings,cargo deny,cargo sort, markdownlint + pre-commit file hooks,Miri (584 passed, 0 failed), cargo-fuzz build + smoke (both targets,
millions of runs, no crashes/leaks).
Deferred