Hi!  SQLite 3.2.1 on win32.

I'm having trouble wrapping my mind around where I have to be
paranoid about locking issues.  In particular, the behavior when
readers and writers overlap, or two writers overlap, is not 100%
clear to me from an obsessive reading and rereading of the locking
documentation, google, the wiki and the mailing list archives.
So here is what I believe; if anything I say is inaccurate, I 
would have undying appreciation for a correction.

Consider the schema:

create table employee (
    id integer primary key,
    name text,
    phone text
);

create table department (
    id integer primary key,
    name text
);

create table emp_dep ( -- many to many relation
    id integer primary key,
    emp_id integer,
    dep_id integer
);

create unique index emp_index on employee(name) on conflict replace;
create unique index dep_index on department(name) on conflict replace;
create unique index emp_dep_index on emp_dep(emp_id, dep_id) 
    on conflict replace;

The operations that will hit this schema in pseudocode:

1. the 'reader' operation (happens asynchronously, often)
        open db
        prepare the following sql statement
        select e.name, d.name from employee e, department d, emp_dep ed
            where e.id = ed.emp_id
            and d.id = ed.dep_id
            and e.name = '?'
        for each employee I'm interested in (1..many)
            execute
            scroll through results
            close the query
        end for
        close sql prepare
        close db

2. the 'writer' operation (happens asynchronously, more rarely)
        open db
        prepare the following sql statements
                A: insert into employee(name, phone) values (?, ?)
                B: insert into department(name) values (?, ?)
                C: insert into emp_dep(emp_id, dep_id) values (?, ?)
        begin deferred transaction
        for each employee I'm interested in (1..many)
                execute A
                save last autoincrement 
                execute B
                save last autoincrement 
                execute C (using the two autoincrements)
        end for
        commit
        close sql prepares
        close db

Here is the reader operation again, this time marked up.  ALL CAPS
means that something else could have a lock preventing the operation
from successfully completing.

        OPEN DB -- a writer could be in the middle of writing
        prepare the following sql statement -- only unsafe if schema changes
        for each employee I'm interested in (1..many)
            EXECUTE -- a writer could have jumped in
            scroll (slowly) through the results -- uninterruptible
            close the query 
        end for
        close sql prepare
        close db

Here is the writer operation, marked up with ALL CAPS for ops 
that could fail due to some other writer or reader.

        OPEN DB -- a writer could be operating
        prepare the following sql statements -- only unsafe if schema changes
        BEGIN DEFERRED TRANSACTION -- a writer or reader could cause BUSY
        for each employee I'm interested in (1..many)
                EXECUTE A -- the transaction could spill into EXCLUSIVE & BUSY
                    -- thus conflicting with an extant SHARED
                SAVE LAST AUTOINCREMENT -- as above
                EXECUTE B -- as above
                SAVE LAST AUTOINCREMENT -- as above
                EXECUTE C -- as above
        end for
        COMMIT -- writer or reader could cause busy
        close sql prepares
        close db

In the comment for 'execute A' above, it seems to me that the pager could
conceivably try to force the transaction into EXCLUSIVE mode at any time
(dependent on memory settings etc. outside the control of the API).
So every call to execute a SQL statement could get the BUSY treatment,
and thus must be explicitly wrapped in retry handling code.

This further suggests that a naive approach to executing sql -- putting
multiple sql statements in one string and passing it to exec -- is
unsafe at any speed; consider

begin deferred transaction;
update...;
update...;
update...;
commit;

if one were to pass that valid SQL to exec, and then get a BUSY back,
there is no recovery possible.  The pager could have decided to force
into exclusive (and thus BUSY) for memory cache spill reasons at any
time (e.g., after the 1st, or 3rd update), OR it could be because of
the commit.  Unless somehow transactions always stay in the memory
cache, which doesn't seem from the documentation to be the case.

In the unlikely event that Dr. Hipp has read this far, it would be
a welcome addition to the documentation to indicate which API functions
may throw exceptions, and which exceptions, in a multi-user scenario.
Additionally, orthogonal to the question of row level locking, it would
be nice to have a set of synchronous APIs that return BUSY only after
a per-connection configurable delay.  And I would also like a pony.

Felix

Reply via email to