Skip to content

v1.1.0

Choose a tag to compare

@WilliamSmithEdward WilliamSmithEdward released this 09 Sep 18:16
· 88 commits to main since this release

Point it at a workbook or an Access database, and query the sheets and
tables inside as if they were a database.

pip install --upgrade pysqlbridge
{
  "tables": [
    { "excel": "data/budget.xlsx" },
    { "access": "data/club.accdb" }
  ]
}

Both hold more than one table, so both produce more than one: a workbook
makes a table per sheet and a database one per table and per saved query,
each named after itself. "sheet" and "table" pick one out.

Nothing to install. Not the Access database engine, which is what
usually reads either of these on Windows: a separate download that installs
in one bit width, refuses to load into a process of the other, and is not on
Linux at all. An .xlsx is a zip of XML the standard library opens, and an
.accdb is read by pyOpenVBA,
which is pure Python with no dependencies of its own. So an Access database
is served on any machine the rest of this runs on, the suite tests it on
every one of them rather than skipping where an engine is missing, and the
single-file executable needs nothing beside it. The file is read once into
memory and never written: no handle is held, no lock file appears, and
somebody can have the same database open in Access while it is being served.

Dates are a real type now. A workbook stores one as a number of days,
and the only thing that makes 45306 a date rather than the number 45306 is
the number format its style points at, so the styles are read. Day 60 is
Excel's 29th of February 1900, a day that did not happen, and is refused
rather than served as some other date; a workbook saved by Excel for Mac
before 2011 counts from 1904 and is read against that epoch. An Access
database declares its types, so a text column stays text where every value
in it happens to be digits, which inference alone gets wrong.

Saved queries are answered where they were written. An Access query says
IIf and Nz and joins its strings with &, none of which is what a
client sends this, so it is run in Access SQL and its rows arrive already
worked out. Only the ones that read: an update or a delete query is a
statement rather than a table, and running one to find out what it returns
would change the file.

Eleven wrong answers, found by having a date column at all. Each was
measured against SQL Server 2025 and added to the differential.
WHERE hired = '2024-01-15' answered nothing, and > was right only by the
accident of an ISO date sorting like its own spelling. A join between an
integer column and the same numbers written as text answered nothing at all,
silently, because the hash it buckets rows by kept the collation and never
crossed a type. A bare column name in a join was refused as one that does
not exist. WHERE hired > 1e18 reached a client as an internal error. A
sheet 1,044 columns wide went past the 1,024 this serves. And @@ROWCOUNT
answered nought whatever had just happened, which every client reads as
"nothing came back".

And two things are quicker. A written IN list evaluated every
candidate for every row, so the cost was rows times values rather than rows
plus values: 2,000 values over 3,200 rows took 452 ms and takes 10, and the
gap widens with every row. A comparison against a written date read the same
characters once a row; remembered, 40,000 rows went from 73 ms to 30.

Everything in
1.0.2
is here unaltered.

2225 tests, on Python 3.10 through 3.14, Linux and Windows. 963 queries
compared against SQL Server 2025 for the same values, the same declared
column types, and the same error numbers where both refuse.