Re: pysqlite design decisions

Gerhard Häring <[email protected]>
Newsgroups gmane.comp.python.db.pysqlite.user
Message-ID <[email protected]>
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

First, thanks for your constructive criticism.

glyph-TyWPi3/[email protected] wrote:
> [...]
> There are actually 3, not 2, layers to pysqlite that I want to separate:
> 
>  1. the SQLite access API
>  2. the transaction management layer
>  3. the statement cache
> 
> It's clear, to me at least, that the higher levels here are inadequately
> tested.  In some cases, if you use them, they will completely break
> features of SQLite itself (for example, ticket #97 - which apparently
> won't ever be fixed...?).

There was no chance to fix it in earlier SQLite releases, now that SQLite
exposes the autocommit mode
(http://sqlite.org/capi3ref.html#sqlite3_get_autocommit), it could be fixed
 for newer SQLite versions.

> I should have spoken up about this sooner, because #170/#185 was clearly
> obvious to Axiom's test suite as of the 2.2 release.

> If there is a real need to optimize the transaction management layer 
> into C then that makes perfect sense, but that should be done *after* a 
> tested version of this has been developed and shaken out in Python.

I didn't think in terms of a transaction management layer so far, mostly
because pysqlite doesn't do anything *so* fancy IMO, only what is required
anyway by the DB-API. Later on, the possibility to switch the

> Access to these features is currently ad-hoc and implicit.

As required by the DB-API.

> Axiom has a huge amount of information about when statements are used
> and re-used, and when transactions should be committed.  This means that
> we want to use the bottom-most layer of pysqlite.  The API for doing
> this is highly misleading: "isolation_level=None" does not actually mean
> "provide a lower isolation level", it means "use SQLite's inherent
> isolation / concurrency primitives rather than those in PySQLite". 

SQLite itself did not use a term for its various locking strategies
(DEFERRED/IMMEDIATE/EXCLUSIVE) that can be controlled via the BEGIN
statement. So I borrowed the term from other databases "isolation level"
that can also controlled via a parameter of the same name in psycopg.

> As far as I can tell it's impossible to exercise any control over the 
> statement cache from Python.

That's true. The statement cache is a different beast and only got
introduced later in version 2.1. In fact, I thought about exposing
"prepared statement" objects before, but my guess was that the builtin
cache from pysqlite is good enough for all practical purposes. If there is
a demand to offer that feature, I think it shouldn't be *that* hard.

> If it were possible for Axiom provide its own statement cache, I am
> fairly sure it would be considerably more efficient than the built-in
> cache, because rather than keeping an in-memory dictionary around and
> looking up strings in it, we could simply associate compiled statement
> objects with query objects.  Right now, we just hang on to the generated
> string, in the hopes that it will be found in pysqlite's internal
> dictionary.  This is spooky action-at-a-distance programming, where what
> I want to do is be guaranteed a hit on the statement cache, because I
> know exactly which statement I'm referring to, and how long it needs to
> live for the user's code to be efficient.  I already have an object to
> attach it to.

Have you benchmarked Axiom with pysqlite 2.0.x and a newer version like
2.3.x? My guess would be that Axiom adds so much overhead anyway that using
prepared statements doesn't make much of a difference anyway. I just want
to make sure we talk about real life problems here.

> Splitting this into a multi-level architecture and exposing each level
> to Python would allow us to use the statement preparation / compilation
> API in the same way that the caching wrapper would, even if there were
> both a C and Python implementation of said wrapper.

Ok, I understand why it would make sense for something like Axiom to not
have two different cache strategies for objects and SQL statements.

> (Just to be clear: I blame this particular misfeature more on the lack
> of a PreparedStatement analogue in DB-API standard than on PySQLite.  I
> can understand if there's no desire to provide a separate, publicly
> supported statement-manipulation API because that's not part of the
> Python DB access spec... 

I don't fear adding additional features on top of the DB-API, but I want to
be as compatible with the DB-API and with previour pysqlite releases as
possible.

> I'd still like it if it were at least
> Python-visible.  I don't mind putting a bit of extra effort in to track
> and support multiple releases, as long as it's possible.)
> 
> Currently (as bug #185 indicated) there are lots of possibilities for
> bugs in the transaction management layer which are difficult to avoid if
> all you need is a simple interface to pysqlite.  Users may want a
> different transaction policy from what pysqlite provides, but the
> options are "use what's built in" or "write a completely separate layer
> on top".

And that sounds like a reasonable policy to me :-)

> I think that pluggability at this level is important, partially because
> I think that the current mechanism for determining when to commit is
> completely broken :).  Someone familiar with SQL databases, even
> familiar with using SQLite from other programming languages, can easily
> write (the Python equivalent of) a transaction like
> 
>   INSERT INTO app_internal_schema_features VALUES('foo');
>   CREATE TABLE foo (bar INTEGER);
>   INSERT INTO foo VALUES (1);
>   COMMIT;

Ok, I see the problem. Unfortunately we need to have pysqlite open
transactions for you, because SQLite doesn't do it for us and the Python
DB-API requires us to do so.

> and unless they've read
> http://www.initd.org/pub/software/pysqlite/doc/usage-guide.html#controlling-transactions
> they will assume that this is going to work the same way it does in
> every other database - which is to say you won't have a 'foo' table
> unless the appropriate corresponding entry has been added to
> app_internal_schema_features.

Ouch.

> [...]
> I mentioned the possibility of bugs.  While writing this email I
> discovered another bug: pysqlite statement detection does not integrate
> with the sqlite parser, it has its own way of determining what type of a
> statement is being executed (string prefixes).  This means that there is
> always the possibility that its idea of what's going on will diverge
> from the actual database engine's.  It takes care of whitespace
> prefixes, but let's say you were loading your SQL statements from
> resource files of some kind and you had a statement like this:
> 
>     -- OK now let's go look up those invoice line items
>     SELECT * FROM invoice_line_item WHERE ...;
> 
> The comment will cause the statement to be identified by PySQLite as a
> "STATEMENT_OTHER" and silently commit any pending transaction - again,
> no way for the developer to notice this.  (I would have submitted this
> as a bug in trac, but it's giving me an "Internal server error" on login
> now.)

Sorry for the trouble - I'm still waiting for the initd.org admins to fix
the webserver problems.

> I believe there is also a bug with "executescript" being detected as the
> statement type of its _first_ statement - unless it begins with a comment.
> 
> While this bug could be fixed, this problem becomes worse when triggers
> and functions come into the mix.  It's not even *possible* to tell what
> sort of statement is going to be run just by looking at the syntax.

It took me a while to understand this. I don't think triggers are a problem
here - there are no triggers that can be, well, triggerred by SELECT. But
it is possible in theory to execute DML or DDL in a SQL function. Of course
that's perverse, considering that a function should have no side effects.

> You might just say "well, don't use triggers, then", just as the current
> transaction-control page advises against using "on conflict rollback"
> but it is possible to use SQLite in applications from more than one
> programming language, and in those situations triggers are used rather
> heavily, to make sure that the different languages are using the
> database in the same way.  Pysqlite could be issuing the same statements
> as tcl or ruby, but behaving completely differently. 

Again, I don't think pysqlite is to blame, but if anything, the DB-API with
its rather strange mixing of concepts of connection and cursors. There
should really have been a separate transaction object in the DB-API, and
cursors are mostly superfluous. They should have been the result of
execute() statements, instead of being created beforehand and containing
the result set, i. e.:

con = sqlite.connect(...)
trans = con.transaction()
trans.execute("insert ...")
cur = trans.execute("select ...")
for row in cur:
  ...
trans.commit()

But I'm disgressing ...

> [...]
> Even if the developer is very concerned with transaction semantics and
> watching a "debug log" of all executed SQL, they won't catch it.  In
> addition to sometimes silently executing its own statements on your
> behalf, the PySQLite wrapper provides no debugging API of its own; most
> libraries make the (otherwise reasonable assumption) that printing
> everything they pass to "execute" is sufficient.

I know of this problem. That's why I have once started with implementing a
logging layer for pysqlite:
http://initd.org/tracker/pysqlite/browser/sandbox/preliminary_logging_patch.dif

Unfortunately, that proved to be rather hard to do in C - I know what's
coming now: so don't do it in C ...

> I would definitely put at least a little time into writing patches, if I
> felt that the direction I was hoping for was supported.  Unfortunately,
> right now my feelings are that all transaction control should simply be
> removed from pysqlite, and that the statement cache should be moved into
> Python.  I don't think patches to do this would currently be accepted.

You're right, I wouldn't like that. Not because the everything-in-C is a
dogma, but because I don't want a grand rewrite. I do think, however, that
most of what you like to see can be implemented in a more iterative
approach and without big changes.

> I would like to understand the motivation behind the way the 
> transactions currently work; why the implicit commits?

As said above, required by the DB-API (the text and the interpretation on
the DB-SIG mailing list).

> What about a
> "CREATE TABLE" statement requires that the database should be committed?

That was really only because in my tests, some DDL commands could safely be
executed in transactions, while SQLite failed for others (maybe now all DDL
commands are transaction safe, I don't know).

> I'm not going to just offer a heap of what's wrong with no suggestions
> for how to make it better though, so here's a plan, which we can perhaps
> come to some agreement on and flesh out:
> 
>  * add a "debug" API which allows you to re-direct the text of all
> executed SQL statements if enabled - and provide appropriate comments
> when BEGIN and COMMIT are executed by the engine.

+1

>  * Re-factor the existing C layer into multiple objects, and expose them
> to Python so that the transaction controller and statement cache can be
> tested independently of actually running SQL.

+0 looks like a lot of effort - if you want to help and we do this in an
experimental branch in SVN, I'm in.

>  * wrap statement compilation and parameter binding for Python

+1

>  * Expose the bottom layer to Python applications (perhaps via a
> different module name?) so that it is easier to get "raw" access to sqlite.

- -0. This is what APSW offers today. It feels unproductive to duplicate all
their efforts. I wouldn't object to just distribute APSW as a submodule
with pysqlite though.

>  * Change the rather vague "isolation_level" parameter to a
> "transaction_policy" parameter, so that the layers can be composed more
> easily, and new policies can be prototyped and profiled in Python before
> C optimization

- -1 on the rename because I don't want any backwards-incompatible changes.
If there really will be a separate layer, it needs to be exposed somehow,
of course.

> I also feel strongly right now that the default transaction policy ought
> to be changed so that DDL statements which happen in the _middle_ of a
> transaction cause an error. 

+1

> At the very least, you ought to force the user to explicitly commit
> before an effective autocommit transaction takes place.  I'd really like
> to hear more about the design rationale behind the way it works now
> before I'd say I'm sure about that though.

If it doesn't become clear after reading the DB-API 2.0 spec, drop me a
note and I will dig in the DB-SIG archives to find relevant posts.

Cheers,

- -- Gerhard
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.3 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iD8DBQFFcwv1dIO4ozGCH14RAtJAAKCCiLMgokW/oqLS4f7P82vaizVhkACdHid4
eHZUu9yiMzLsRFwsu3abIQ8=
=W0mt
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.