Skip to content

v1.0.1

Choose a tag to compare

@WilliamSmithEdward WilliamSmithEdward released this 08 Sep 19:24
· 93 commits to main since this release

A patch release. Every fix here was found by hunting rather than
reported: fuzzing the SQL, the socket and the bound parameters, sweeping
sources and misbehaving HTTP servers, and timing query shapes at two
sizes. Each was measured against SQL Server 2025 before it was changed.

pip install --upgrade pysqlbridge

Four crashes that reached a client as an internal error. A lone
surrogate in a JSON response took down any query that touched the row; it
now goes on the wire as the code unit, which is what SQL Server stores
and returns. A column named in a JOIN's ON condition escaped as an
internal error where the same mistake in a WHERE had always been a
refusal. A response that stops part way through, which is a proxy
truncating, raised out of the read. And text bound to TOP (@n) reached
int() with no message written for it.

Two answers that were quietly wrong. TOP (-1) returned every row
but the last, taken as a slice bound, where a real server says msg 127. A
column that does not exist was reported as msg 208, invalid object name,
which is the number for a missing table and sends whoever reads it
looking for the wrong thing; SQL Server says 207 in the select list, a
WHERE, an ON and an ORDER BY alike.

A form of INSERT that did not work. INSERT INTO #t VALUES (1, 'x')
came back saying it had produced no rows. Only the form taking a SELECT
worked. Single rows, several at once, named and partial column lists,
computed rows and rows holding parameters all work now, and the two
errors carry the numbers SQL Server gives them.

Data no longer lost in silence. A source with two columns that are
one name under this collation, or a column with no name at all, served
them and let only the first be read: refused now, in SQL Server's own
words. A number too big for a bigint kept its digits as text instead of
failing to encode. A file that is not UTF-8 names itself and the reason
rather than raising the decoder's error. A scratch table made twice, or
dropped when it was never made, said nothing where a real server says msg
2714 and msg 3701.

Three quadratics, now linear. A running total over a window worked
the whole frame out again for every row: 2.08 seconds over sixteen
thousand rows, 0.02 now. IN (SELECT ...) walked the inner column per
outer row: 0.39 seconds over two thousand against two thousand, 0.004
now. A correlated EXISTS read the inner table per outer row: 5.2
seconds, 0.004 now.

Two answers brought in line with SQL Server. Totals are added one
value at a time rather than with Python's compensated sum, because a real
server does: over 1e16, 1, 1 and -1e16 it answers 0 where sum() answers
2. Spread is worked out from running sums rather than from the mean, for
the same reason.

Also here: GRANT, REVOKE and DENY are refused rather than reported
as having worked; a select written in brackets is answered rather than
silently returning nothing; OPTION (...) is passed over; a query run
again is logged again; and a CSV can say what separates it.

2028 tests, on Python 3.10 through 3.14, Linux and Windows. 890 queries
compared against SQL Server 2025.